generated from nhcarrigan/template
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8ccc3f4d0d
|
|||
|
2c34fe2c81
|
|||
|
5025948530
|
@@ -1,135 +0,0 @@
|
|||||||
# Vampire Expansion — Implementation TODO
|
|
||||||
|
|
||||||
Branch: `feat/expansions`
|
|
||||||
|
|
||||||
Thematic currency names:
|
|
||||||
- Gold → **Blood**
|
|
||||||
- Essence → **Ichor**
|
|
||||||
- Crystals → **Soul Shards**
|
|
||||||
- Runestones → **Bloodstones**
|
|
||||||
- Echoes → **Whispers**
|
|
||||||
- Click action → **Hunt**
|
|
||||||
- Adventurers → **Thralls**
|
|
||||||
- Prestige → **Siring** (working name)
|
|
||||||
- Transcendence → **The Awakening** (working name)
|
|
||||||
- Apotheosis → **Eternal Sovereignty** (role ID: 1486144657023959180)
|
|
||||||
|
|
||||||
CDN prefix for all vampire art: `https://cdn.nhcarrigan.com/elysium/vampire/<folder>/<id>.jpg`
|
|
||||||
Local scratch dir (delete before committing): `img/vampire/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 — Types
|
|
||||||
|
|
||||||
- [ ] Add `VampireExpansionState` interface to `packages/types/src/interfaces/` mirroring full `GameState` structure (zones, bosses, quests, adventurers, upgrades, equipment, achievements, prestige, transcendence, apotheosis, exploration, resources, baseClickPower, lastTickAt, dailyChallenges, codex, autoQuest, autoBoss, autoAdventurer, companions, story)
|
|
||||||
- [ ] Add `ExpansionsState` interface: `{ vampire?: VampireExpansionState }`
|
|
||||||
- [ ] Add `expansions?: ExpansionsState` field to `GameState`
|
|
||||||
- [ ] Export new types from `packages/types/src/index.ts`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 — Data files (vampire content)
|
|
||||||
|
|
||||||
All data files go in `apps/api/src/data/vampire/`.
|
|
||||||
Same content scale as base game; use vampire theming throughout.
|
|
||||||
|
|
||||||
- [ ] `zones.ts` — 18 vampire-themed zones (crypts, blood forests, cursed castles, etc.)
|
|
||||||
- [ ] `bosses.ts` — 72 vampire-themed bosses (4 per zone)
|
|
||||||
- [ ] `quests.ts` — match base game quest count (~95); vampire-themed names/descriptions
|
|
||||||
- [ ] `adventurers.ts` — 32 thrall tiers with progressive stats
|
|
||||||
- [ ] `upgrades.ts` — match base game upgrade count (~57); vampire-themed
|
|
||||||
- [ ] `equipment.ts` — match base game equipment count (~53); vampire-themed sets
|
|
||||||
- [ ] `equipmentSets.ts` — vampire equipment sets
|
|
||||||
- [ ] `achievements.ts` — match base game count (~40); vampire-themed conditions
|
|
||||||
- [ ] `explorations.ts` — 72 areas across 18 vampire lore zones
|
|
||||||
- [ ] `materials.ts` — match base game material count (~54); vampire-themed
|
|
||||||
- [ ] `recipes.ts` — match base game recipe count (~36); vampire-themed
|
|
||||||
- [ ] `prestigeUpgrades.ts` — 25 "Siring" upgrades
|
|
||||||
- [ ] `transcendenceUpgrades.ts` — 15 "Awakening" upgrades
|
|
||||||
- [ ] `dailyChallenges.ts` — 10 vampire daily challenges
|
|
||||||
- [ ] `initialState.ts` — `initialVampireState()` function mirroring `initialGameState` structure
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 — Art generation & CDN upload
|
|
||||||
|
|
||||||
For each category below, generate images via Gemini API (`gemini-3-pro-image-preview`),
|
|
||||||
save locally to `img/vampire/<folder>/`, upload to R2, then delete local files.
|
|
||||||
|
|
||||||
Use soft-shaded anime style; vampire/gothic aesthetic; crimson/black/dark purple palette.
|
|
||||||
|
|
||||||
- [ ] Zone banners (18) → `img/vampire/zones/` → CDN `vampire/zones/`
|
|
||||||
- [ ] Boss portraits (72) → `img/vampire/bosses/` → CDN `vampire/bosses/`
|
|
||||||
- [ ] Quest banners (match count) → `img/vampire/quests/` → CDN `vampire/quests/`
|
|
||||||
- [ ] Adventurer/thrall portraits (32) → `img/vampire/adventurers/` → CDN `vampire/adventurers/`
|
|
||||||
- [ ] Equipment icons (match count) → `img/vampire/equipment/` → CDN `vampire/equipment/`
|
|
||||||
- [ ] Achievement icons (match count) → `img/vampire/achievements/` → CDN `vampire/achievements/`
|
|
||||||
- [ ] Exploration area art (72) → `img/vampire/explorations/` → CDN `vampire/explorations/`
|
|
||||||
- [ ] Material icons (match count) → `img/vampire/materials/` → CDN `vampire/materials/`
|
|
||||||
- [ ] Story chapter banners (match count) → `img/vampire/story-chapters/` → CDN `vampire/story-chapters/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4 — API changes
|
|
||||||
|
|
||||||
- [ ] Add `inGuild` to Prisma `Player` model → update `initialGameState` if needed (already done in #134 — verify migration)
|
|
||||||
- [ ] Update Prisma schema: no DB changes needed (expansion state is inside the `GameState` JSON blob)
|
|
||||||
- [ ] Update `initialState.ts` to include `expansions: {}` in `initialGameState`
|
|
||||||
- [ ] Update `sync-new-content` debug route to inject/patch vampire expansion content when expansion is unlocked
|
|
||||||
- [ ] Add vampire-specific unlock trigger: when base-game apotheosis count ≥ 1, set `expansions.vampire` to `initialVampireState()` and `unlocked: true`
|
|
||||||
- [ ] Update the load endpoint to pass expansion state through to the client
|
|
||||||
- [ ] Ensure prestige/transcendence/apotheosis routes only reset state for their own expansion (base game routes must NOT touch `expansions.*`)
|
|
||||||
- [ ] Add vampire prestige, transcendence, and apotheosis routes (mirrors of base game routes, scoped to `expansions.vampire`)
|
|
||||||
- [ ] Grant `Eternal Sovereignty` role (ID: `1486144657023959180`) on vampire apotheosis
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5 — Frontend changes
|
|
||||||
|
|
||||||
### Expansion switcher
|
|
||||||
- [ ] Add expansion toggle buttons below the Early Access warning in the sidebar
|
|
||||||
- [ ] Always render all expansion buttons; disable any where `unlocked !== true`
|
|
||||||
- [ ] Active expansion stored in React state (not game state); defaults to `"base"`
|
|
||||||
- [ ] Switching expansion updates which data the UI panels display
|
|
||||||
|
|
||||||
### Resource bar
|
|
||||||
- [ ] Show ALL currencies from ALL expansions as separate labelled lines
|
|
||||||
- [ ] Vampire currencies use distinct icons/colours (crimson tint for blood, etc.)
|
|
||||||
- [ ] The "expand" button label shows the gold-equivalent currency of the active expansion
|
|
||||||
|
|
||||||
### Thematic UI
|
|
||||||
- [ ] When vampire expansion is active, swap labels: gold → Blood, essence → Ichor, etc.
|
|
||||||
- [ ] Apply `.vampire-mode` CSS class to game container when vampire is active
|
|
||||||
- [ ] Vampire colour palette: deep crimsons (`#5C0A1A`), rich crimson (`#C41E3A`), blacks, desaturated purples
|
|
||||||
|
|
||||||
### Tick engine
|
|
||||||
- [ ] Update `apps/web/src/engine/tick.ts` to compute passive income for all unlocked expansions every tick (not just base game)
|
|
||||||
- [ ] Offline income calculation must also cover all expansions
|
|
||||||
|
|
||||||
### Profile
|
|
||||||
- [ ] Profile panel: tab stats by expansion (base game tab + one tab per unlocked expansion)
|
|
||||||
- [ ] Show correct thematic prestige/transcendence/apotheosis badge names per expansion
|
|
||||||
- [ ] Lifetime stats (gold earned, clicks, etc.) tracked separately per expansion
|
|
||||||
|
|
||||||
### About / How to Play
|
|
||||||
- [ ] Update `aboutPanel.tsx` `HOW_TO_PLAY` array to document the expansion system
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6 — Tests & CI
|
|
||||||
|
|
||||||
- [ ] Unit tests for all new data files (at minimum, validate structure/required fields)
|
|
||||||
- [ ] Unit tests for `initialVampireState()`
|
|
||||||
- [ ] Tests for vampire unlock trigger route
|
|
||||||
- [ ] Tests for vampire prestige/transcendence/apotheosis routes
|
|
||||||
- [ ] Tests for updated tick engine (expansion income)
|
|
||||||
- [ ] Maintain 100% coverage on `apps/api` and `packages/types`
|
|
||||||
- [ ] Full pipeline: lint → build → test passing before PR
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7 — Final
|
|
||||||
|
|
||||||
- [ ] Delete `img/vampire/` directory before committing
|
|
||||||
- [ ] Update `MEMORY.md` with new content counts
|
|
||||||
- [ ] Open PR → request review
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@elysium/api",
|
"name": "@elysium/api",
|
||||||
"version": "0.3.2",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./prod/src/index.js",
|
"main": "./prod/src/index.js",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
DISCORD_CLIENT_ID="op://Environment Variables - Naomi/Elysium/discord client id"
|
||||||
DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Elysium/discord client secret"
|
DISCORD_CLIENT_SECRET="op://Environment Variables - Naomi/Elysium/discord client secret"
|
||||||
|
DISCORD_REDIRECT_URI="op://Environment Variables - Naomi/Elysium/discord redirect uri"
|
||||||
JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret"
|
JWT_SECRET="op://Environment Variables - Naomi/Elysium/jwt secret"
|
||||||
DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url"
|
DATABASE_URL="op://Environment Variables - Naomi/Elysium/mongo url"
|
||||||
ANTI_CHEAT_SECRET="op://Environment Variables - Naomi/Elysium/anti cheat secret"
|
ANTI_CHEAT_SECRET="op://Environment Variables - Naomi/Elysium/anti cheat secret"
|
||||||
@@ -6,4 +8,6 @@ PORT="op://Environment Variables - Naomi/Elysium/port"
|
|||||||
CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin"
|
CORS_ORIGIN="op://Environment Variables - Naomi/Elysium/origin"
|
||||||
DISCORD_MILESTONE_WEBHOOK="op://Environment Variables - Naomi/Elysium/discord milestone webhook"
|
DISCORD_MILESTONE_WEBHOOK="op://Environment Variables - Naomi/Elysium/discord milestone webhook"
|
||||||
DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Elysium/discord bot token"
|
DISCORD_BOT_TOKEN="op://Environment Variables - Naomi/Elysium/discord bot token"
|
||||||
|
DISCORD_GUILD_ID="op://Environment Variables - Naomi/Elysium/discord guild id"
|
||||||
|
DISCORD_APOTHEOSIS_ROLE_ID="op://Environment Variables - Naomi/Elysium/discord apotheosis role id"
|
||||||
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
|
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"
|
||||||
@@ -7,9 +7,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention -- Discord API requires snake_case fields and HTTP headers require Pascal-Case */
|
/* eslint-disable @typescript-eslint/naming-convention -- Discord API requires snake_case fields and HTTP headers require Pascal-Case */
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const discordClientId = "1479551654264049908";
|
|
||||||
const discordRedirectUri = "https://elysium.nhcarrigan.com/api/auth/callback";
|
|
||||||
|
|
||||||
interface DiscordTokenResponse {
|
interface DiscordTokenResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
@@ -34,18 +31,24 @@ interface DiscordUser {
|
|||||||
const exchangeCode = async(
|
const exchangeCode = async(
|
||||||
code: string,
|
code: string,
|
||||||
): Promise<DiscordTokenResponse> => {
|
): Promise<DiscordTokenResponse> => {
|
||||||
|
const clientId = process.env.DISCORD_CLIENT_ID;
|
||||||
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
|
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
|
||||||
|
const redirectUri = process.env.DISCORD_REDIRECT_URI;
|
||||||
|
|
||||||
if (clientSecret === undefined || clientSecret === "") {
|
if (
|
||||||
|
clientId === undefined || clientId === ""
|
||||||
|
|| clientSecret === undefined || clientSecret === ""
|
||||||
|
|| redirectUri === undefined || redirectUri === ""
|
||||||
|
) {
|
||||||
throw new Error("Discord OAuth environment variables are required");
|
throw new Error("Discord OAuth environment variables are required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = new URLSearchParams({
|
const parameters = new URLSearchParams({
|
||||||
client_id: discordClientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
code: code,
|
code: code,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
redirect_uri: discordRedirectUri,
|
redirect_uri: redirectUri,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -143,9 +146,19 @@ const fetchDiscordUserById = async(
|
|||||||
* @throws {Error} If OAuth environment variables are missing.
|
* @throws {Error} If OAuth environment variables are missing.
|
||||||
*/
|
*/
|
||||||
const buildOAuthUrl = (): string => {
|
const buildOAuthUrl = (): string => {
|
||||||
|
const clientId = process.env.DISCORD_CLIENT_ID;
|
||||||
|
const redirectUri = process.env.DISCORD_REDIRECT_URI;
|
||||||
|
|
||||||
|
if (
|
||||||
|
clientId === undefined || clientId === ""
|
||||||
|
|| redirectUri === undefined || redirectUri === ""
|
||||||
|
) {
|
||||||
|
throw new Error("Discord OAuth environment variables are required");
|
||||||
|
}
|
||||||
|
|
||||||
const parameters = new URLSearchParams({
|
const parameters = new URLSearchParams({
|
||||||
client_id: discordClientId,
|
client_id: clientId,
|
||||||
redirect_uri: discordRedirectUri,
|
redirect_uri: redirectUri,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: "identify",
|
scope: "identify",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,6 @@
|
|||||||
import { prisma } from "../db/client.js";
|
import { prisma } from "../db/client.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const discordGuildId = "1354624415861833870";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord Gateway opcodes used by this client.
|
* Discord Gateway opcodes used by this client.
|
||||||
*/
|
*/
|
||||||
@@ -38,7 +36,8 @@ const handleGuildMemberAdd = async(
|
|||||||
discordId: string,
|
discordId: string,
|
||||||
guildId: string,
|
guildId: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (guildId !== discordGuildId) {
|
const configuredGuildId = process.env.DISCORD_GUILD_ID;
|
||||||
|
if (guildId !== configuredGuildId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +66,8 @@ const handleGuildMemberRemove = async(
|
|||||||
discordId: string,
|
discordId: string,
|
||||||
guildId: string,
|
guildId: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (guildId !== discordGuildId) {
|
const configuredGuildId = process.env.DISCORD_GUILD_ID;
|
||||||
|
if (guildId !== configuredGuildId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ const suppressNotifications = 4096;
|
|||||||
/**
|
/**
|
||||||
* The Discord role ID for the Elysian role granted to all Elysium players.
|
* The Discord role ID for the Elysian role granted to all Elysium players.
|
||||||
*/
|
*/
|
||||||
const discordGuildId = "1354624415861833870";
|
|
||||||
const elysianRoleId = "1486144823684628490";
|
const elysianRoleId = "1486144823684628490";
|
||||||
const apotheosisRoleId = "1479966598210129991";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grants the Elysian Discord role to the given player and returns whether they are in the guild.
|
* Grants the Elysian Discord role to the given player and returns whether they are in the guild.
|
||||||
@@ -30,14 +28,18 @@ const apotheosisRoleId = "1479966598210129991";
|
|||||||
*/
|
*/
|
||||||
const grantElysianRole = async(discordId: string): Promise<boolean> => {
|
const grantElysianRole = async(discordId: string): Promise<boolean> => {
|
||||||
const botToken = process.env.DISCORD_BOT_TOKEN;
|
const botToken = process.env.DISCORD_BOT_TOKEN;
|
||||||
|
const guildId = process.env.DISCORD_GUILD_ID;
|
||||||
|
|
||||||
if (botToken === undefined || botToken === "") {
|
if (
|
||||||
|
botToken === undefined || botToken === ""
|
||||||
|
|| guildId === undefined || guildId === ""
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${discordApi}/guilds/${discordGuildId}/members/${discordId}/roles/${elysianRoleId}`,
|
`${discordApi}/guilds/${guildId}/members/${discordId}/roles/${elysianRoleId}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bot ${botToken}`,
|
"Authorization": `Bot ${botToken}`,
|
||||||
@@ -66,14 +68,20 @@ const grantElysianRole = async(discordId: string): Promise<boolean> => {
|
|||||||
*/
|
*/
|
||||||
const grantApotheosisRole = async(discordId: string): Promise<void> => {
|
const grantApotheosisRole = async(discordId: string): Promise<void> => {
|
||||||
const botToken = process.env.DISCORD_BOT_TOKEN;
|
const botToken = process.env.DISCORD_BOT_TOKEN;
|
||||||
|
const guildId = process.env.DISCORD_GUILD_ID;
|
||||||
|
const roleId = process.env.DISCORD_APOTHEOSIS_ROLE_ID;
|
||||||
|
|
||||||
if (botToken === undefined || botToken === "") {
|
if (
|
||||||
|
botToken === undefined || botToken === ""
|
||||||
|
|| guildId === undefined || guildId === ""
|
||||||
|
|| roleId === undefined || roleId === ""
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(
|
await fetch(
|
||||||
`${discordApi}/guilds/${discordGuildId}/members/${discordId}/roles/${apotheosisRoleId}`,
|
`${discordApi}/guilds/${guildId}/members/${discordId}/roles/${roleId}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": `Bot ${botToken}`,
|
"Authorization": `Bot ${botToken}`,
|
||||||
|
|||||||
@@ -18,31 +18,51 @@ describe("discord service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("buildOAuthUrl", () => {
|
describe("buildOAuthUrl", () => {
|
||||||
|
it("throws when DISCORD_CLIENT_ID is missing", async () => {
|
||||||
|
delete process.env["DISCORD_CLIENT_ID"];
|
||||||
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/callback";
|
||||||
|
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
||||||
|
expect(() => buildOAuthUrl()).toThrow("Discord OAuth environment variables are required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when DISCORD_REDIRECT_URI is missing", async () => {
|
||||||
|
process.env["DISCORD_CLIENT_ID"] = "client123";
|
||||||
|
delete process.env["DISCORD_REDIRECT_URI"];
|
||||||
|
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
||||||
|
expect(() => buildOAuthUrl()).toThrow("Discord OAuth environment variables are required");
|
||||||
|
});
|
||||||
|
|
||||||
it("returns a URL with correct query params", async () => {
|
it("returns a URL with correct query params", async () => {
|
||||||
|
process.env["DISCORD_CLIENT_ID"] = "client123";
|
||||||
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/callback";
|
||||||
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
const { buildOAuthUrl } = await import("../../src/services/discord.js");
|
||||||
const url = buildOAuthUrl();
|
const url = buildOAuthUrl();
|
||||||
expect(url).toContain("client_id=1479551654264049908");
|
expect(url).toContain("client_id=client123");
|
||||||
expect(url).toContain("response_type=code");
|
expect(url).toContain("response_type=code");
|
||||||
expect(url).toContain("scope=identify");
|
expect(url).toContain("scope=identify");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("exchangeCode", () => {
|
describe("exchangeCode", () => {
|
||||||
it("throws when DISCORD_CLIENT_SECRET is missing", async () => {
|
it("throws when env vars are missing", async () => {
|
||||||
delete process.env["DISCORD_CLIENT_SECRET"];
|
delete process.env["DISCORD_CLIENT_ID"];
|
||||||
const { exchangeCode } = await import("../../src/services/discord.js");
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
||||||
await expect(exchangeCode("mycode")).rejects.toThrow("Discord OAuth environment variables are required");
|
await expect(exchangeCode("mycode")).rejects.toThrow("Discord OAuth environment variables are required");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when response is not ok", async () => {
|
it("throws when response is not ok", async () => {
|
||||||
|
process.env["DISCORD_CLIENT_ID"] = "cid";
|
||||||
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
||||||
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
|
||||||
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized" });
|
mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized" });
|
||||||
const { exchangeCode } = await import("../../src/services/discord.js");
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
||||||
await expect(exchangeCode("bad_code")).rejects.toThrow("Discord token exchange failed");
|
await expect(exchangeCode("bad_code")).rejects.toThrow("Discord token exchange failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns parsed body on success", async () => {
|
it("returns parsed body on success", async () => {
|
||||||
|
process.env["DISCORD_CLIENT_ID"] = "cid";
|
||||||
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
||||||
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
|
||||||
const tokenData = { access_token: "tok", token_type: "Bearer", expires_in: 3600, refresh_token: "ref", scope: "identify" };
|
const tokenData = { access_token: "tok", token_type: "Bearer", expires_in: 3600, refresh_token: "ref", scope: "identify" };
|
||||||
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(tokenData) });
|
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(tokenData) });
|
||||||
const { exchangeCode } = await import("../../src/services/discord.js");
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
||||||
@@ -76,7 +96,9 @@ describe("discord service", () => {
|
|||||||
|
|
||||||
describe("exchangeCode non-Error throw", () => {
|
describe("exchangeCode non-Error throw", () => {
|
||||||
it("re-throws when fetch rejects with a non-Error value", async () => {
|
it("re-throws when fetch rejects with a non-Error value", async () => {
|
||||||
|
process.env["DISCORD_CLIENT_ID"] = "cid";
|
||||||
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
process.env["DISCORD_CLIENT_SECRET"] = "secret";
|
||||||
|
process.env["DISCORD_REDIRECT_URI"] = "http://localhost/cb";
|
||||||
mockFetch.mockRejectedValueOnce("raw string error");
|
mockFetch.mockRejectedValueOnce("raw string error");
|
||||||
const { exchangeCode } = await import("../../src/services/discord.js");
|
const { exchangeCode } = await import("../../src/services/discord.js");
|
||||||
await expect(exchangeCode("some_code")).rejects.toBe("raw string error");
|
await expect(exchangeCode("some_code")).rejects.toBe("raw string error");
|
||||||
|
|||||||
@@ -16,48 +16,60 @@ vi.mock("../../src/services/logger.js", () => ({
|
|||||||
|
|
||||||
import { prisma } from "../../src/db/client.js";
|
import { prisma } from "../../src/db/client.js";
|
||||||
|
|
||||||
const discordGuildId = "1354624415861833870";
|
|
||||||
|
|
||||||
describe("gateway service", () => {
|
describe("gateway service", () => {
|
||||||
|
const ORIGINAL_ENV = process.env;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
process.env = ORIGINAL_ENV;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("handleGuildMemberAdd", () => {
|
describe("handleGuildMemberAdd", () => {
|
||||||
it("sets inGuild to true for the matching guild", async () => {
|
it("sets inGuild to true for the matching guild", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
|
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
|
||||||
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
||||||
await handleGuildMemberAdd("user123", discordGuildId);
|
await handleGuildMemberAdd("user123", "guild123");
|
||||||
expect(prisma.player.updateMany).toHaveBeenCalledWith({
|
expect(prisma.player.updateMany).toHaveBeenCalledWith({
|
||||||
data: { inGuild: true },
|
data: { inGuild: true },
|
||||||
where: { discordId: "user123" },
|
where: { discordId: "user123" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("no-ops when guild id does not match the configured guild", async () => {
|
it("no-ops when guild id does not match", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
||||||
await handleGuildMemberAdd("user123", "other_guild");
|
await handleGuildMemberAdd("user123", "other_guild");
|
||||||
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("no-ops when DISCORD_GUILD_ID env var is missing and guild does not match undefined", async () => {
|
||||||
|
delete process.env["DISCORD_GUILD_ID"];
|
||||||
|
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
||||||
|
await handleGuildMemberAdd("user123", "guild123");
|
||||||
|
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("logs error when prisma throws an Error", async () => {
|
it("logs error when prisma throws an Error", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
const dbError = new Error("DB failure");
|
const dbError = new Error("DB failure");
|
||||||
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
|
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
|
||||||
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
||||||
const { logger } = await import("../../src/services/logger.js");
|
const { logger } = await import("../../src/services/logger.js");
|
||||||
await handleGuildMemberAdd("user123", discordGuildId);
|
await handleGuildMemberAdd("user123", "guild123");
|
||||||
expect(logger.error).toHaveBeenCalledWith("gateway_member_add", dbError);
|
expect(logger.error).toHaveBeenCalledWith("gateway_member_add", dbError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs error when prisma throws a non-Error", async () => {
|
it("logs error when prisma throws a non-Error", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
|
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
|
||||||
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberAdd } = await import("../../src/services/gateway.js");
|
||||||
const { logger } = await import("../../src/services/logger.js");
|
const { logger } = await import("../../src/services/logger.js");
|
||||||
await handleGuildMemberAdd("user123", discordGuildId);
|
await handleGuildMemberAdd("user123", "guild123");
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
"gateway_member_add",
|
"gateway_member_add",
|
||||||
new Error("raw error"),
|
new Error("raw error"),
|
||||||
@@ -67,35 +79,46 @@ describe("gateway service", () => {
|
|||||||
|
|
||||||
describe("handleGuildMemberRemove", () => {
|
describe("handleGuildMemberRemove", () => {
|
||||||
it("sets inGuild to false for the matching guild", async () => {
|
it("sets inGuild to false for the matching guild", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
|
vi.mocked(prisma.player.updateMany).mockResolvedValueOnce({ count: 1 });
|
||||||
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
||||||
await handleGuildMemberRemove("user123", discordGuildId);
|
await handleGuildMemberRemove("user123", "guild123");
|
||||||
expect(prisma.player.updateMany).toHaveBeenCalledWith({
|
expect(prisma.player.updateMany).toHaveBeenCalledWith({
|
||||||
data: { inGuild: false },
|
data: { inGuild: false },
|
||||||
where: { discordId: "user123" },
|
where: { discordId: "user123" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("no-ops when guild id does not match the configured guild", async () => {
|
it("no-ops when guild id does not match", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
||||||
await handleGuildMemberRemove("user123", "other_guild");
|
await handleGuildMemberRemove("user123", "other_guild");
|
||||||
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("no-ops when DISCORD_GUILD_ID env var is missing", async () => {
|
||||||
|
delete process.env["DISCORD_GUILD_ID"];
|
||||||
|
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
||||||
|
await handleGuildMemberRemove("user123", "guild123");
|
||||||
|
expect(prisma.player.updateMany).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("logs error when prisma throws an Error", async () => {
|
it("logs error when prisma throws an Error", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
const dbError = new Error("DB failure");
|
const dbError = new Error("DB failure");
|
||||||
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
|
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce(dbError);
|
||||||
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
||||||
const { logger } = await import("../../src/services/logger.js");
|
const { logger } = await import("../../src/services/logger.js");
|
||||||
await handleGuildMemberRemove("user123", discordGuildId);
|
await handleGuildMemberRemove("user123", "guild123");
|
||||||
expect(logger.error).toHaveBeenCalledWith("gateway_member_remove", dbError);
|
expect(logger.error).toHaveBeenCalledWith("gateway_member_remove", dbError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs error when prisma throws a non-Error", async () => {
|
it("logs error when prisma throws a non-Error", async () => {
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
|
vi.mocked(prisma.player.updateMany).mockRejectedValueOnce("raw error");
|
||||||
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
const { handleGuildMemberRemove } = await import("../../src/services/gateway.js");
|
||||||
const { logger } = await import("../../src/services/logger.js");
|
const { logger } = await import("../../src/services/logger.js");
|
||||||
await handleGuildMemberRemove("user123", discordGuildId);
|
await handleGuildMemberRemove("user123", "guild123");
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
expect(logger.error).toHaveBeenCalledWith(
|
||||||
"gateway_member_remove",
|
"gateway_member_remove",
|
||||||
new Error("raw error"),
|
new Error("raw error"),
|
||||||
|
|||||||
@@ -20,20 +20,42 @@ describe("webhook service", () => {
|
|||||||
describe("grantApotheosisRole", () => {
|
describe("grantApotheosisRole", () => {
|
||||||
it("does nothing when bot token is missing", async () => {
|
it("does nothing when bot token is missing", async () => {
|
||||||
delete process.env["DISCORD_BOT_TOKEN"];
|
delete process.env["DISCORD_BOT_TOKEN"];
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
|
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role123";
|
||||||
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
await grantApotheosisRole("user123");
|
await grantApotheosisRole("user123");
|
||||||
expect(mockFetch).not.toHaveBeenCalled();
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls Discord API with correct URL and auth when bot token is set", async () => {
|
it("does nothing when guild id is missing", async () => {
|
||||||
|
process.env["DISCORD_BOT_TOKEN"] = "token";
|
||||||
|
delete process.env["DISCORD_GUILD_ID"];
|
||||||
|
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role123";
|
||||||
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
|
await grantApotheosisRole("user123");
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing when role id is missing", async () => {
|
||||||
|
process.env["DISCORD_BOT_TOKEN"] = "token";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
|
delete process.env["DISCORD_APOTHEOSIS_ROLE_ID"];
|
||||||
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
|
await grantApotheosisRole("user123");
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls Discord API with correct URL and auth when env vars are set", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
|
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "role456";
|
||||||
mockFetch.mockResolvedValueOnce({ ok: true });
|
mockFetch.mockResolvedValueOnce({ ok: true });
|
||||||
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
await grantApotheosisRole("user789");
|
await grantApotheosisRole("user789");
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
"https://discord.com/api/v10/guilds/1354624415861833870/members/user789/roles/1479966598210129991",
|
"https://discord.com/api/v10/guilds/guild123/members/user789/roles/role456",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
|
headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -41,6 +63,8 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("swallows fetch errors gracefully", async () => {
|
it("swallows fetch errors gracefully", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
|
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "r";
|
||||||
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
||||||
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
|
await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
|
||||||
@@ -48,6 +72,8 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("swallows non-Error fetch rejections gracefully", async () => {
|
it("swallows non-Error fetch rejections gracefully", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
|
process.env["DISCORD_APOTHEOSIS_ROLE_ID"] = "r";
|
||||||
mockFetch.mockRejectedValueOnce("raw string error");
|
mockFetch.mockRejectedValueOnce("raw string error");
|
||||||
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
const { grantApotheosisRole } = await import("../../src/services/webhook.js");
|
||||||
await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
|
await expect(grantApotheosisRole("user")).resolves.toBeUndefined();
|
||||||
@@ -57,6 +83,18 @@ describe("webhook service", () => {
|
|||||||
describe("grantElysianRole", () => {
|
describe("grantElysianRole", () => {
|
||||||
it("does nothing when bot token is missing", async () => {
|
it("does nothing when bot token is missing", async () => {
|
||||||
delete process.env["DISCORD_BOT_TOKEN"];
|
delete process.env["DISCORD_BOT_TOKEN"];
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
|
process.env["DISCORD_ELYSIAN_ROLE_ID"] = "role123";
|
||||||
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
|
const result = await grantElysianRole("user123");
|
||||||
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing when guild id is missing", async () => {
|
||||||
|
process.env["DISCORD_BOT_TOKEN"] = "token";
|
||||||
|
delete process.env["DISCORD_GUILD_ID"];
|
||||||
|
process.env["DISCORD_ELYSIAN_ROLE_ID"] = "role123";
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user123");
|
const result = await grantElysianRole("user123");
|
||||||
expect(mockFetch).not.toHaveBeenCalled();
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
@@ -65,11 +103,12 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("returns true when Discord API responds with ok", async () => {
|
it("returns true when Discord API responds with ok", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
process.env["DISCORD_BOT_TOKEN"] = "bot_token";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "guild123";
|
||||||
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user789");
|
const result = await grantElysianRole("user789");
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
"https://discord.com/api/v10/guilds/1354624415861833870/members/user789/roles/1486144823684628490",
|
"https://discord.com/api/v10/guilds/guild123/members/user789/roles/1486144823684628490",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
|
headers: expect.objectContaining({ Authorization: "Bot bot_token" }),
|
||||||
@@ -80,6 +119,7 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("returns true when Discord API responds with 204", async () => {
|
it("returns true when Discord API responds with 204", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
mockFetch.mockResolvedValueOnce({ ok: false, status: 204 });
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 204 });
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user");
|
const result = await grantElysianRole("user");
|
||||||
@@ -88,6 +128,7 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("returns false when Discord API responds with an error status", async () => {
|
it("returns false when Discord API responds with an error status", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
mockFetch.mockResolvedValueOnce({ ok: false, status: 403 });
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 403 });
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user");
|
const result = await grantElysianRole("user");
|
||||||
@@ -96,6 +137,7 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("returns false and swallows fetch errors gracefully", async () => {
|
it("returns false and swallows fetch errors gracefully", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user");
|
const result = await grantElysianRole("user");
|
||||||
@@ -104,6 +146,7 @@ describe("webhook service", () => {
|
|||||||
|
|
||||||
it("returns false and swallows non-Error fetch rejections", async () => {
|
it("returns false and swallows non-Error fetch rejections", async () => {
|
||||||
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
process.env["DISCORD_BOT_TOKEN"] = "tok";
|
||||||
|
process.env["DISCORD_GUILD_ID"] = "g";
|
||||||
mockFetch.mockRejectedValueOnce("raw string error");
|
mockFetch.mockRejectedValueOnce("raw string error");
|
||||||
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
const { grantElysianRole } = await import("../../src/services/webhook.js");
|
||||||
const result = await grantElysianRole("user");
|
const result = await grantElysianRole("user");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@elysium/web",
|
"name": "@elysium/web",
|
||||||
"version": "0.3.2",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ export const GameProvider = ({
|
|||||||
setSchemaOutdated(data.schemaOutdated);
|
setSchemaOutdated(data.schemaOutdated);
|
||||||
setSaveSchemaVersion(data.state.schemaVersion ?? 0);
|
setSaveSchemaVersion(data.state.schemaVersion ?? 0);
|
||||||
setCurrentSchemaVersion(data.currentSchemaVersion);
|
setCurrentSchemaVersion(data.currentSchemaVersion);
|
||||||
setInGuild(data.inGuild);
|
setInGuild(data.inGuild === true);
|
||||||
|
|
||||||
// Fetch number format preference from profile (fire-and-forget, non-blocking)
|
// Fetch number format preference from profile (fire-and-forget, non-blocking)
|
||||||
void fetch(`/api/profile/${data.state.player.discordId}`).
|
void fetch(`/api/profile/${data.state.player.discordId}`).
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "elysium",
|
"name": "elysium",
|
||||||
"version": "0.3.2",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@elysium/types",
|
"name": "@elysium/types",
|
||||||
"version": "0.3.2",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./prod/src/index.js",
|
"main": "./prod/src/index.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user