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
68 changes: 67 additions & 1 deletion index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,72 @@ 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);
});

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 () => {
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,
Expand Down Expand Up @@ -126,7 +192,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",
);
});

Expand Down
5 changes: 5 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,7 @@ export type FlagStatus = {

export interface UserContext {
cohort?: string | number | boolean;
env?: string;
}

export interface RocketFlagClient {
Expand All @@ -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}`);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down