From fe4206b8a839213170a57c674ebca048009cd1ba Mon Sep 17 00:00:00 2001 From: shadrach68 Date: Mon, 15 Jun 2026 21:29:33 +0100 Subject: [PATCH] fix: safely encode dynamic URL path segments --- src/guilds/guilds.service.ts | 5 ++-- src/roles/roles.service.ts | 7 +++-- src/utils/formatting.ts | 12 +++++++++ tests/services.test.ts | 51 ++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/guilds/guilds.service.ts b/src/guilds/guilds.service.ts index 1038296..daa4d5d 100644 --- a/src/guilds/guilds.service.ts +++ b/src/guilds/guilds.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '../http/httpClient'; // GuildPass SDK: Pull in package or module bindings. import { validateGuildId } from '../utils/validation'; +import { encodePathSegment } from '../utils/formatting'; // GuildPass SDK: Import external module dependencies. import { GetGuildParams, Guild, GuildConfig } from './guilds.types'; @@ -20,7 +21,7 @@ export class GuildsService { validateGuildId(guildId); // GuildPass SDK: Send back computed results to the caller. - return this.http.get(`/guilds/${guildId}`); + return this.http.get(`/guilds/${encodePathSegment(guildId)}`); // GuildPass SDK: End of logic containment structure block. } @@ -34,7 +35,7 @@ export class GuildsService { validateGuildId(guildId); // GuildPass SDK: Return evaluated output value. - return this.http.get(`/guilds/${guildId}/config`); + return this.http.get(`/guilds/${encodePathSegment(guildId)}/config`); // GuildPass SDK: End of logic containment structure block. } // GuildPass SDK: End of logic containment structure block. diff --git a/src/roles/roles.service.ts b/src/roles/roles.service.ts index fcb0380..d946a0f 100644 --- a/src/roles/roles.service.ts +++ b/src/roles/roles.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '../http/httpClient'; // GuildPass SDK: Import external module dependencies. import { validateAddress, validateGuildId } from '../utils/validation'; +import { encodePathSegment } from '../utils/formatting'; // GuildPass SDK: Pull in package or module bindings. import { GetRolesParams, GetUserRolesParams, GuildRole } from './roles.types'; @@ -20,7 +21,7 @@ export class RolesService { validateGuildId(guildId); // GuildPass SDK: Return evaluated output value. - return this.http.get(`/guilds/${guildId}/roles`); + return this.http.get(`/guilds/${encodePathSegment(guildId)}/roles`); // GuildPass SDK: End of logic containment structure block. } @@ -36,7 +37,9 @@ export class RolesService { validateGuildId(guildId); // GuildPass SDK: Terminate function block execution and return. - return this.http.get(`/guilds/${guildId}/members/${walletAddress}/roles`); + return this.http.get( + `/guilds/${encodePathSegment(guildId)}/members/${encodePathSegment(walletAddress)}/roles`, + ); // GuildPass SDK: End of logic containment structure block. } // GuildPass SDK: End of logic containment structure block. diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index acc531b..ad618a5 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -25,3 +25,15 @@ export const capitalise = (str: string): string => { return str.charAt(0).toUpperCase() + str.slice(1); // GuildPass SDK: End of logic containment structure block. }; + +/** + * Encodes a path segment for safe insertion into URL paths. + * + * @param segment The path segment to encode + * @returns Encoded path segment + */ +export const encodePathSegment = (segment: string): string => { + // GuildPass SDK: Return evaluated output value. + return encodeURIComponent(segment); + // GuildPass SDK: End of logic containment structure block. +}; diff --git a/tests/services.test.ts b/tests/services.test.ts index 9c46381..e27b257 100644 --- a/tests/services.test.ts +++ b/tests/services.test.ts @@ -69,6 +69,40 @@ describe('Service Modules', () => { expect.any(Object), ); }); + + it('should URL-encode guild IDs in role endpoint paths', async () => { + const mockRoles = [{ id: '1', name: 'Role 1' }]; + (fetch as any).mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockRoles), + headers: new Headers(), + }); + + const result = await client.roles.getRoles({ guildId: 'guild/1' }); + expect(result).toEqual(mockRoles); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/guilds/guild%2F1/roles'), + expect.any(Object), + ); + }); + + it('should URL-encode wallet addresses and guild IDs in user roles endpoint paths', async () => { + const mockRoles = [{ id: '1', name: 'Role 1' }]; + (fetch as any).mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockRoles), + headers: new Headers(), + }); + + const result = await client.roles.getUserRoles({ guildId: 'guild/1', walletAddress: '0x123/456 space' }); + expect(result).toEqual(mockRoles); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/guilds/guild%2F1/members/0x123%2F456%20space/roles'), + expect.any(Object), + ); + }); }); describe('GuildsService', () => { @@ -84,5 +118,22 @@ describe('Service Modules', () => { const result = await client.guilds.getGuild({ guildId: 'guild_1' }); expect(result).toEqual(mockGuild); }); + + it('should URL-encode guild IDs in guild endpoint paths', async () => { + const mockGuild = { id: 'guild/1', name: 'Encoded Guild' }; + (fetch as any).mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve(mockGuild), + headers: new Headers(), + }); + + const result = await client.guilds.getGuild({ guildId: 'guild/1' }); + expect(result).toEqual(mockGuild); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/guilds/guild%2F1'), + expect.any(Object), + ); + }); }); });