From 18accb739e1d9ae82df9a1cd7a83358c933264e2 Mon Sep 17 00:00:00 2001 From: JK Gunnink Date: Fri, 12 Sep 2025 13:59:39 +0800 Subject: [PATCH 1/3] add check for alphanumeric characters --- index.test.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- index.ts | 5 +++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/index.test.ts b/index.test.ts index 1679e3b..de97265 100644 --- a/index.test.ts +++ b/index.test.ts @@ -77,6 +77,53 @@ describe("createRocketflagClient", () => { expect(flag).toEqual(mockFlag); }); + it("should fetch a flag with env in the user context", async () => { + const mockFlag: FlagStatus = { name: "Test Flag", enabled: true, id: flagId }; + (fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve(mockFlag) }); + + const client = createRocketflagClient(); + const flag = await client.getFlag(flagId, { env: "staging" }); + + expect(fetch).toHaveBeenCalledTimes(1); + + const expectedUrl = `${apiUrl}/v1/flags/${flagId}?env=staging`; + const expectedURLObject = new URL(expectedUrl); + + expect(fetch).toHaveBeenCalledWith(expect.objectContaining({ href: expectedURLObject.href }), { method: "GET" }); + expect(flag).toEqual(mockFlag); + }); + + it("should fetch a flag with cohort and env in the user context", async () => { + const mockFlag: FlagStatus = { name: "Test Flag", enabled: true, id: flagId }; + (fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve(mockFlag) }); + + const client = createRocketflagClient(); + const flag = await client.getFlag(flagId, { cohort: "user123", env: "staging" }); + + expect(fetch).toHaveBeenCalledTimes(1); + + const expectedUrl = `${apiUrl}/v1/flags/${flagId}?cohort=user123&env=staging`; + const expectedURLObject = new URL(expectedUrl); + + expect(fetch).toHaveBeenCalledWith(expect.objectContaining({ href: expectedURLObject.href }), { method: "GET" }); + expect(flag).toEqual(mockFlag); + }); + + it("should throw an error if env contains non-alphanumeric characters", async () => { + const client = createRocketflagClient(); + await expect(client.getFlag(flagId, { env: "staging+test@rocketflag.com" })).rejects.toThrow( + "env values must be alphanumeric. Invalid value for env: env", + ); + }); + + it("should throw an error if env in userContext contains invalid values", async () => { + const client = createRocketflagClient(); + const invalidUserContext = { env: { a: 1 } }; + await expect(client.getFlag(flagId, invalidUserContext as unknown as UserContext)).rejects.toThrow( + "userContext values must be of type string, number, or boolean. Invalid value for key: env", + ); + }); + it("should throw an APIError on non-ok response with correct status and statusText", async () => { (fetch as jest.Mock).mockResolvedValue({ ok: false, @@ -126,7 +173,7 @@ describe("createRocketflagClient", () => { const client = createRocketflagClient(); const invalidUserContext = { cohort: { a: 1 } }; await expect(client.getFlag(flagId, invalidUserContext as unknown as UserContext)).rejects.toThrow( - "userContext values must be of type string, number, or boolean. Invalid value for key: cohort" + "userContext values must be of type string, number, or boolean. Invalid value for key: cohort", ); }); diff --git a/index.ts b/index.ts index 0de535b..f25a752 100644 --- a/index.ts +++ b/index.ts @@ -4,6 +4,7 @@ import { validateFlag } from "./validateFlag"; const GET_METHOD = "GET"; const DEFAULT_API_URL = "https://api.rocketflag.app"; const DEFAULT_VERSION = "v1"; +const ALPHANUMERIC_REGEX = /^[a-zA-Z0-9]+$/; export type FlagStatus = { name: string; @@ -13,6 +14,7 @@ export type FlagStatus = { export interface UserContext { cohort?: string | number | boolean; + env?: string; } export interface RocketFlagClient { @@ -36,6 +38,9 @@ const createRocketflagClient = (version = DEFAULT_VERSION, apiUrl = DEFAULT_API_ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") { throw new Error(`userContext values must be of type string, number, or boolean. Invalid value for key: ${key}`); } + if (key === "env" && (typeof value !== "string" || !ALPHANUMERIC_REGEX.test(value))) { + throw new Error(`env values must be alphanumeric. Invalid value for env: ${[key]}`); + } } const url = new URL(`${apiUrl}/${version}/flags/${flagId}`); From 11252c59bb2748c23c90b450f7c8461a653361e7 Mon Sep 17 00:00:00 2001 From: JK Gunnink Date: Fri, 12 Sep 2025 14:00:05 +0800 Subject: [PATCH 2/3] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a13293..313e426 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocketflag/node-sdk", - "version": "1.0.0", + "version": "1.1.0", "author": "RocketFlag Developers (https://rocketflag.app)", "main": "index.js", "scripts": { From cd6e7cc1ace6792fc54dd81eae3c24fc9ffa2591 Mon Sep 17 00:00:00 2001 From: JK Gunnink Date: Fri, 12 Sep 2025 14:45:50 +0800 Subject: [PATCH 3/3] update test cases to cover more cases --- index.test.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/index.test.ts b/index.test.ts index de97265..69ef0ee 100644 --- a/index.test.ts +++ b/index.test.ts @@ -109,11 +109,30 @@ describe("createRocketflagClient", () => { expect(flag).toEqual(mockFlag); }); - it("should throw an error if env contains non-alphanumeric characters", async () => { - const client = createRocketflagClient(); - await expect(client.getFlag(flagId, { env: "staging+test@rocketflag.com" })).rejects.toThrow( - "env values must be alphanumeric. Invalid value for env: env", - ); + describe("userContext validation", () => { + it.each([ + { value: "staging", shouldThrow: false }, + { value: "123", shouldThrow: false }, + { value: "production1", shouldThrow: false }, + { value: "test2", shouldThrow: false }, + { value: "staging test", shouldThrow: true }, + { value: "staging-test", shouldThrow: true }, + { value: "staging!", shouldThrow: true }, + { value: "staging@test", shouldThrow: true }, + { value: "staging+test@rocketflag.com", shouldThrow: true }, + ])("should handle env value: $value", async ({ value, shouldThrow }) => { + const client = createRocketflagClient(); + + if (shouldThrow) { + await expect(client.getFlag(flagId, { env: value })).rejects.toThrow( + "env values must be alphanumeric. Invalid value for env: env", + ); + } else { + const mockFlag: FlagStatus = { name: "Test Flag", enabled: true, id: flagId }; + (fetch as jest.Mock).mockResolvedValue({ ok: true, json: () => Promise.resolve(mockFlag) }); + await expect(client.getFlag(flagId, { env: value })).resolves.toEqual(mockFlag); + } + }); }); it("should throw an error if env in userContext contains invalid values", async () => {