feature/IO-3701-Harness-Replacement - Implement

This commit is contained in:
Dave
2026-05-20 14:41:24 -04:00
parent 84ec68f142
commit deb2fc28ce
100 changed files with 4813 additions and 773 deletions

View File

@@ -0,0 +1,44 @@
const CREATE_FIELDS = ["name", "description", "default_treatment", "active"];
const UPDATE_FIELDS = ["description", "default_treatment", "active"];
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
/**
* Trims stable feature flag identity fields while leaving free-form text unchanged.
*/
const normalizeStringField = (key, value) => {
if (value == null) return value;
if (key !== "name" && key !== "default_treatment") return value;
return typeof value === "string" ? value.trim() : value;
};
/**
* Whitelists fields accepted by feature flag definition create/update admin endpoints.
*/
const pickFeatureFlagFields = (input, allowedFields) => {
if (!input || typeof input !== "object" || Array.isArray(input)) {
return {};
}
return allowedFields.reduce((payload, field) => {
if (hasOwn(input, field)) {
payload[field] = normalizeStringField(field, input[field]);
}
return payload;
}, {});
};
/**
* Builds a safe payload for creating a feature flag definition.
*/
const sanitizeFeatureFlagCreatePayload = (input) => pickFeatureFlagFields(input, CREATE_FIELDS);
/**
* Builds a safe payload for updating editable feature flag definition fields.
*/
const sanitizeFeatureFlagUpdatePayload = (input) => pickFeatureFlagFields(input, UPDATE_FIELDS);
module.exports = {
sanitizeFeatureFlagCreatePayload,
sanitizeFeatureFlagUpdatePayload
};

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const {
sanitizeFeatureFlagCreatePayload,
sanitizeFeatureFlagUpdatePayload
} = require("./admin-payload");
describe("feature flag admin payload sanitizing", () => {
it("keeps only editable create fields and trims stable text keys", () => {
expect(
sanitizeFeatureFlagCreatePayload({
name: " TEST_FLAG ",
description: "Manual test flag",
default_treatment: " variant-a ",
active: false,
created_at: "2026-05-19T00:00:00.000Z",
bodyshop_feature_flags_aggregate: { aggregate: { count: 3 } }
})
).toEqual({
name: "TEST_FLAG",
description: "Manual test flag",
default_treatment: "variant-a",
active: false
});
});
it("strips name from update payloads so flag keys cannot be renamed", () => {
expect(
sanitizeFeatureFlagUpdatePayload({
name: "Renamed_Flag",
description: null,
default_treatment: " on ",
active: true,
updated_at: "2026-05-19T00:00:00.000Z"
})
).toEqual({
description: null,
default_treatment: "on",
active: true
});
});
it("returns an empty update payload when no editable fields are present", () => {
expect(
sanitizeFeatureFlagUpdatePayload({
name: "Only_Name",
created_at: "2026-05-19T00:00:00.000Z"
})
).toEqual({});
});
});

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { buildImportSql, normalizeTreatment, sqlString } = require("../../scripts/export-harness-feature-flags");
describe("Harness feature flag exporter", () => {
it("preserves custom treatment names while normalizing booleans and known treatments", () => {
expect(normalizeTreatment(true)).toBe("on");
expect(normalizeTreatment(false)).toBe("off");
expect(normalizeTreatment(" ON ")).toBe("on");
expect(normalizeTreatment("false")).toBe("off");
expect(normalizeTreatment("control")).toBe("control");
expect(normalizeTreatment("variant-a")).toBe("variant-a");
expect(normalizeTreatment(" custom treatment ")).toBe("custom treatment");
expect(normalizeTreatment(null)).toBe("control");
expect(normalizeTreatment("")).toBe("control");
});
it("escapes SQL string values", () => {
expect(sqlString("Dave's Shop")).toBe("'Dave''s Shop'");
});
it("escapes custom treatments in generated import SQL", () => {
const sql = buildImportSql([
{
customerKey: "SHOP'1",
imexshopid: "SHOP'1",
name: "Demo'Flag",
treatment: "pilot's-choice",
config: { text: "Dave's config" }
}
]);
expect(sql).toContain("('SHOP''1', 'Demo''Flag', 'pilot''s-choice'");
expect(sql).toContain(`'{"text":"Dave''s config"}'::jsonb`);
});
it("includes an unmatched feature flag report query", () => {
const sql = buildImportSql([
{
customerKey: "SHOP1",
imexshopid: "SHOP1",
name: "Missing_Flag",
treatment: "on",
config: null
}
]);
expect(sql).toContain('AS "unmatched_feature_flag"');
expect(sql).toContain('LEFT JOIN "public"."feature_flags"');
expect(sql).toContain("('Missing_Flag')");
});
});

View File

@@ -0,0 +1,144 @@
const logger = require("../utils/logger");
const { CHECK_BODYSHOP_ACCESS, GET_BODYSHOP_FEATURE_FLAGS } = require("../graphql-client/queries");
const { emitFeatureFlagsChanged } = require("./socket-events");
/**
* Indicates whether verbose feature flag route logging should be enabled.
*/
const isDevelopment = () => process.env.NODE_ENV === "development";
/**
* Combines global feature flag definitions with per-bodyshop assignments into the runtime flag map.
*/
const toFlagMap = ({ feature_flags: definitions = [], bodyshop_feature_flags: assignments = [] }) => {
const flags = definitions.reduce((acc, definition) => {
acc[definition.name] = {
treatment: definition.default_treatment || "off",
config: null
};
return acc;
}, {});
for (const assignment of assignments) {
flags[assignment.name] = {
treatment: assignment.treatment,
config: assignment.config ?? null,
activeDate: assignment.activeDate ?? null,
deactiveDate: assignment.deactiveDate ?? null
};
}
return flags;
};
/**
* Verifies that the authenticated user can read the requested bodyshop through Hasura permissions.
*/
async function assertBodyshopAccess({ req, bodyshopId }) {
const result = await req.userGraphQLClient.request(CHECK_BODYSHOP_ACCESS, { id: bodyshopId });
if (!result.bodyshops_by_pk?.id) {
const error = new Error("Feature flag bodyshop access denied");
error.statusCode = 403;
throw error;
}
}
/**
* Serves runtime feature flags for one bodyshop with Redis read-through caching.
*/
async function getBodyshopFeatureFlags(req, res) {
const bodyshopId = req.params.bodyshopId;
const {
getBodyshopFeatureFlagsCacheVersion,
getBodyshopFeatureFlagsFromRedis,
setBodyshopFeatureFlagsInRedis
} = req.sessionUtils || {};
try {
await assertBodyshopAccess({ req, bodyshopId });
const cacheVersion = getBodyshopFeatureFlagsCacheVersion
? await getBodyshopFeatureFlagsCacheVersion()
: null;
const cachedFlags = getBodyshopFeatureFlagsFromRedis
? await getBodyshopFeatureFlagsFromRedis(bodyshopId, cacheVersion)
: null;
if (cachedFlags) {
if (isDevelopment()) {
logger.log("feature-flags-route-hit", "DEBUG", req.user?.email, null, {
bodyshopId,
source: "redis",
flagCount: Object.keys(cachedFlags.flags || {}).length
});
}
return res.json({ ...cachedFlags, source: "redis" });
}
const result = await req.userGraphQLClient.request(GET_BODYSHOP_FEATURE_FLAGS, { bodyshopid: bodyshopId });
const payload = {
bodyshopId,
flags: toFlagMap(result),
cachedAt: new Date().toISOString()
};
if (setBodyshopFeatureFlagsInRedis) {
await setBodyshopFeatureFlagsInRedis(bodyshopId, payload, cacheVersion);
}
if (isDevelopment()) {
logger.log("feature-flags-route-hit", "DEBUG", req.user?.email, null, {
bodyshopId,
source: "database",
flagCount: Object.keys(payload.flags || {}).length
});
}
return res.json({ ...payload, source: "database" });
} catch (error) {
const statusCode = error.statusCode || 500;
logger.log("get-bodyshop-feature-flags-error", "ERROR", req.user?.email, null, {
bodyshopId,
statusCode,
error: error.message,
stack: error.stack
});
return res.status(statusCode).json({ error: error.message });
}
}
/**
* Handles Hasura/admin cache invalidation events and notifies connected clients.
*/
async function invalidateBodyshopFeatureFlags(req, res) {
const bodyshopId = req.body?.event?.data?.new?.bodyshopid || req.body?.event?.data?.old?.bodyshopid || req.body?.bodyshopid;
const tableName = req.body?.event?.table?.name;
const flagName = req.body?.event?.data?.new?.name || req.body?.event?.data?.old?.name || req.body?.name || null;
try {
if (bodyshopId && req.sessionUtils?.invalidateBodyshopFeatureFlagsInRedis) {
await req.sessionUtils.invalidateBodyshopFeatureFlagsInRedis(bodyshopId);
emitFeatureFlagsChanged({ req, bodyshopId, source: "hasura", table: tableName, name: flagName });
return res.status(200).json({ ok: true, bodyshopId });
}
const invalidated = await req.sessionUtils?.invalidateAllBodyshopFeatureFlagsInRedis?.();
emitFeatureFlagsChanged({ req, source: "hasura", table: tableName, name: flagName });
return res.status(200).json({ ok: true, table: tableName, cacheVersion: invalidated || 0 });
} catch (error) {
logger.log("invalidate-bodyshop-feature-flags-error", "ERROR", "feature-flags", null, {
bodyshopId,
tableName,
error: error.message,
stack: error.stack
});
return res.status(500).json({ error: error.message });
}
}
module.exports = {
getBodyshopFeatureFlags,
invalidateBodyshopFeatureFlags
};

View File

@@ -0,0 +1,197 @@
import { describe, expect, it, vi } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { getBodyshopFeatureFlags, invalidateBodyshopFeatureFlags } = require("./feature-flags");
/**
* Creates the minimal Express response mock needed by feature flag route tests.
*/
const createResponse = () => {
const res = {
body: null,
code: 200,
json: vi.fn((body) => {
res.body = body;
return res;
}),
status: vi.fn((code) => {
res.code = code;
return res;
})
};
return res;
};
describe("feature flag runtime route", () => {
it("returns cached flags without re-querying runtime assignments", async () => {
const req = {
params: { bodyshopId: "shop-1" },
sessionUtils: {
getBodyshopFeatureFlagsCacheVersion: vi.fn(async () => "7"),
getBodyshopFeatureFlagsFromRedis: vi.fn(async () => ({
bodyshopId: "shop-1",
flags: {
Demo: { treatment: "on", config: null }
}
}))
},
user: { email: "tester@example.com" },
userGraphQLClient: {
request: vi.fn(async () => ({ bodyshops_by_pk: { id: "shop-1" } }))
}
};
const res = createResponse();
await getBodyshopFeatureFlags(req, res);
expect(req.sessionUtils.getBodyshopFeatureFlagsFromRedis).toHaveBeenCalledWith("shop-1", "7");
expect(req.userGraphQLClient.request).toHaveBeenCalledTimes(1);
expect(res.body).toEqual({
bodyshopId: "shop-1",
flags: {
Demo: { treatment: "on", config: null }
},
source: "redis"
});
});
it("merges active definitions with bodyshop assignments on cache miss", async () => {
const req = {
params: { bodyshopId: "shop-1" },
sessionUtils: {
getBodyshopFeatureFlagsCacheVersion: vi.fn(async () => "3"),
getBodyshopFeatureFlagsFromRedis: vi.fn(async () => null),
setBodyshopFeatureFlagsInRedis: vi.fn()
},
user: { email: "tester@example.com" },
userGraphQLClient: {
request: vi
.fn()
.mockResolvedValueOnce({ bodyshops_by_pk: { id: "shop-1" } })
.mockResolvedValueOnce({
feature_flags: [
{ name: "Default_Off", default_treatment: "off" },
{ name: "Default_Custom", default_treatment: "variant-a" }
],
bodyshop_feature_flags: [
{
name: "Default_Off",
treatment: "on",
config: { limit: 10 },
activeDate: "2026-05-19T15:00:00.000Z",
deactiveDate: null
}
]
})
}
};
const res = createResponse();
await getBodyshopFeatureFlags(req, res);
expect(req.sessionUtils.setBodyshopFeatureFlagsInRedis).toHaveBeenCalledWith(
"shop-1",
expect.objectContaining({
bodyshopId: "shop-1",
flags: {
Default_Off: {
treatment: "on",
config: { limit: 10 },
activeDate: "2026-05-19T15:00:00.000Z",
deactiveDate: null
},
Default_Custom: {
treatment: "variant-a",
config: null
}
}
}),
"3"
);
expect(res.body).toEqual(
expect.objectContaining({
bodyshopId: "shop-1",
flags: {
Default_Off: {
treatment: "on",
config: { limit: 10 },
activeDate: "2026-05-19T15:00:00.000Z",
deactiveDate: null
},
Default_Custom: {
treatment: "variant-a",
config: null
}
},
source: "database"
})
);
});
});
describe("feature flag cache invalidation route", () => {
it("invalidates one bodyshop when a bodyshop assignment event is received", async () => {
const emit = vi.fn();
const req = {
body: {
event: {
table: { name: "bodyshop_feature_flags" },
data: {
new: { bodyshopid: "shop-1" }
}
}
},
ioHelpers: {
getBodyshopRoom: vi.fn(() => "bodyshop-room-shop-1")
},
ioRedis: {
to: vi.fn(() => ({ emit }))
},
sessionUtils: {
invalidateBodyshopFeatureFlagsInRedis: vi.fn()
}
};
const res = createResponse();
await invalidateBodyshopFeatureFlags(req, res);
expect(req.sessionUtils.invalidateBodyshopFeatureFlagsInRedis).toHaveBeenCalledWith("shop-1");
expect(req.ioRedis.to).toHaveBeenCalledWith("bodyshop-room-shop-1");
expect(emit).toHaveBeenCalledWith(
"feature-flags-changed",
expect.objectContaining({ bodyshopId: "shop-1", scope: "bodyshop", source: "hasura", table: "bodyshop_feature_flags" })
);
expect(res.body).toEqual({ ok: true, bodyshopId: "shop-1" });
});
it("bumps global cache version when a feature definition event is received", async () => {
const req = {
body: {
event: {
table: { name: "feature_flags" },
data: {
new: { name: "Demo" }
}
}
},
ioRedis: {
emit: vi.fn()
},
sessionUtils: {
invalidateAllBodyshopFeatureFlagsInRedis: vi.fn(async () => 12)
}
};
const res = createResponse();
await invalidateBodyshopFeatureFlags(req, res);
expect(req.sessionUtils.invalidateAllBodyshopFeatureFlagsInRedis).toHaveBeenCalled();
expect(req.ioRedis.emit).toHaveBeenCalledWith(
"feature-flags-changed",
expect.objectContaining({ name: "Demo", scope: "global", source: "hasura", table: "feature_flags" })
);
expect(res.body).toEqual({ ok: true, table: "feature_flags", cacheVersion: 12 });
});
});

View File

@@ -0,0 +1,40 @@
const FEATURE_FLAGS_CHANGED_EVENT = "feature-flags-changed";
/**
* Creates the Socket.IO payload used to tell browsers that feature flags changed.
*/
const createFeatureFlagsChangedPayload = ({ bodyshopId = null, source = "unknown", table = null, name = null } = {}) => ({
bodyshopId,
changedAt: new Date().toISOString(),
name,
scope: bodyshopId ? "bodyshop" : "global",
source,
table
});
/**
* Emits a feature-flag change event globally or to one bodyshop room.
*/
const emitFeatureFlagsChanged = ({ req, bodyshopId = null, source = "unknown", table = null, name = null } = {}) => {
const io = req?.ioRedis;
if (!io) return null;
const payload = createFeatureFlagsChangedPayload({ bodyshopId, source, table, name });
if (bodyshopId) {
const room = req?.ioHelpers?.getBodyshopRoom
? req.ioHelpers.getBodyshopRoom(bodyshopId)
: `bodyshop-broadcast-room:${bodyshopId}`;
io.to(room).emit(FEATURE_FLAGS_CHANGED_EVENT, payload);
return payload;
}
io.emit(FEATURE_FLAGS_CHANGED_EVENT, payload);
return payload;
};
module.exports = {
FEATURE_FLAGS_CHANGED_EVENT,
createFeatureFlagsChangedPayload,
emitFeatureFlagsChanged
};

View File

@@ -0,0 +1,60 @@
import { describe, expect, it, vi } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const {
FEATURE_FLAGS_CHANGED_EVENT,
createFeatureFlagsChangedPayload,
emitFeatureFlagsChanged
} = require("./socket-events");
describe("feature flag socket events", () => {
it("creates a global payload when no bodyshop id is provided", () => {
expect(createFeatureFlagsChangedPayload({ source: "admin", table: "feature_flags", name: "Demo" })).toEqual(
expect.objectContaining({
bodyshopId: null,
name: "Demo",
scope: "global",
source: "admin",
table: "feature_flags"
})
);
});
it("emits bodyshop-scoped changes to the bodyshop room", () => {
const emit = vi.fn();
const req = {
ioHelpers: {
getBodyshopRoom: vi.fn(() => "bodyshop-room-shop-1")
},
ioRedis: {
to: vi.fn(() => ({ emit }))
}
};
const payload = emitFeatureFlagsChanged({
req,
bodyshopId: "shop-1",
source: "hasura",
table: "bodyshop_feature_flags",
name: "Demo"
});
expect(req.ioRedis.to).toHaveBeenCalledWith("bodyshop-room-shop-1");
expect(emit).toHaveBeenCalledWith(FEATURE_FLAGS_CHANGED_EVENT, payload);
expect(payload).toEqual(expect.objectContaining({ bodyshopId: "shop-1", scope: "bodyshop" }));
});
it("broadcasts global changes to all sockets", () => {
const req = {
ioRedis: {
emit: vi.fn()
}
};
const payload = emitFeatureFlagsChanged({ req, source: "admin", table: "feature_flags" });
expect(req.ioRedis.emit).toHaveBeenCalledWith(FEATURE_FLAGS_CHANGED_EVENT, payload);
expect(payload).toEqual(expect.objectContaining({ bodyshopId: null, scope: "global" }));
});
});