Files
scrying-pool/tests/unit/contracts/privacy-settings.test.js
T
2026-05-24 00:37:21 +02:00

571 lines
20 KiB
JavaScript

/**
* Tests for PrivacySettings contract.
* @module tests/unit/contracts/privacy-settings.test
*/
import {
PRIVACY_SETTINGS_DEFAULT,
PRIVACY_SETTINGS_VERSION,
PRIVACY_SETTING_KEYS,
FEATURE_NAME_MAP,
MAX_PORTRAIT_SIZE,
VALID_PORTRAIT_FORMATS,
createPrivacySettings,
isValidPrivacySettings,
validateSettingKey,
validateSettingValue,
validateFeatureName,
validatePortraitDataURL,
} from "../../../src/contracts/privacy-settings.js";
import { describe, it, expect } from "vitest";
describe("privacy-settings contract", () => {
describe("constants", () => {
it("should export PRIVACY_SETTINGS_VERSION as 1", () => {
expect(PRIVACY_SETTINGS_VERSION).toBe(1);
});
it("should export PRIVACY_SETTINGS_DEFAULT with all false and null portrait", () => {
expect(PRIVACY_SETTINGS_DEFAULT).toEqual({
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
it("should export PRIVACY_SETTING_KEYS as frozen array including portrait fallback", () => {
expect(PRIVACY_SETTING_KEYS).toEqual([
"reactionCamEnabled",
"hpReactiveCamStylingEnabled",
"customPortraitFallback",
]);
expect(Object.isFrozen(PRIVACY_SETTING_KEYS)).toBe(true);
});
it("should export FEATURE_NAME_MAP as frozen object", () => {
expect(FEATURE_NAME_MAP).toEqual({
reactionCam: "reactionCamEnabled",
hpReactiveCamStyling: "hpReactiveCamStylingEnabled",
});
expect(Object.isFrozen(FEATURE_NAME_MAP)).toBe(true);
});
});
describe("createPrivacySettings", () => {
it("should return default settings when no overrides provided", () => {
const result = createPrivacySettings();
expect(result).toEqual(PRIVACY_SETTINGS_DEFAULT);
});
it("should merge overrides with defaults", () => {
const result = createPrivacySettings({ reactionCamEnabled: true });
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
it("should allow both boolean settings to be overridden", () => {
const result = createPrivacySettings({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
});
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
customPortraitFallback: null,
});
});
it("should allow customPortraitFallback to be overridden", () => {
const dataURL = "data:image/png;base64,test";
const result = createPrivacySettings({ customPortraitFallback: dataURL });
expect(result).toEqual({
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: dataURL,
});
});
it("should not modify the defaults object", () => {
const originalDefaults = { ...PRIVACY_SETTINGS_DEFAULT };
createPrivacySettings({ reactionCamEnabled: true });
expect(PRIVACY_SETTINGS_DEFAULT).toEqual(originalDefaults);
});
it("should ignore extra properties in overrides", () => {
const result = createPrivacySettings({
reactionCamEnabled: true,
extraProp: "should be ignored",
});
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
});
describe("isValidPrivacySettings", () => {
it("should accept valid settings with all false", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
it("should accept valid settings with all true", () => {
const valid = {
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
it("should accept valid settings with mixed values", () => {
const valid = {
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
it("should throw TypeError for null", () => {
expect(() => isValidPrivacySettings(null)).toThrow(TypeError);
expect(() => isValidPrivacySettings(null)).toThrow(
"PrivacySettings: must be an object"
);
});
it("should throw TypeError for undefined", () => {
expect(() => isValidPrivacySettings(undefined)).toThrow(TypeError);
expect(() => isValidPrivacySettings(undefined)).toThrow(
"PrivacySettings: must be an object"
);
});
it("should throw TypeError for string", () => {
expect(() => isValidPrivacySettings("invalid")).toThrow(TypeError);
expect(() => isValidPrivacySettings("invalid")).toThrow(
"PrivacySettings: must be an object"
);
});
it("should throw TypeError for array", () => {
expect(() => isValidPrivacySettings([])).toThrow(TypeError);
expect(() => isValidPrivacySettings([])).toThrow(
"PrivacySettings: must be an object"
);
});
it("should throw TypeError for unknown keys", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
extraKey: "invalid",
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow("unknown keys");
});
it("should throw TypeError when reactionCamEnabled is not boolean", () => {
const invalid = {
reactionCamEnabled: "not a boolean",
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"reactionCamEnabled must be a boolean"
);
});
it("should throw TypeError when reactionCamEnabled is a number", () => {
const invalid = {
reactionCamEnabled: 1,
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"reactionCamEnabled must be a boolean"
);
});
it("should throw TypeError when hpReactiveCamStylingEnabled is not boolean", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: "not a boolean",
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"hpReactiveCamStylingEnabled must be a boolean"
);
});
it("should throw TypeError when hpReactiveCamStylingEnabled is null", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: null,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"hpReactiveCamStylingEnabled must be a boolean"
);
});
it("should accept settings with only reactionCamEnabled (backward compatible)", () => {
// Backward compatibility: settings without all keys are accepted
const valid = {
reactionCamEnabled: false,
};
expect(() => isValidPrivacySettings(valid)).not.toThrow();
});
it("should accept settings with only hpReactiveCamStylingEnabled (backward compatible)", () => {
// Backward compatibility: settings without all keys are accepted
const valid = {
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(valid)).not.toThrow();
});
it("should accept empty object (backward compatible)", () => {
// Backward compatibility: empty object is accepted
expect(() => isValidPrivacySettings({})).not.toThrow();
});
});
describe("validateSettingKey", () => {
it("should accept valid key: reactionCamEnabled", () => {
expect(validateSettingKey("reactionCamEnabled")).toBe("reactionCamEnabled");
});
it("should accept valid key: hpReactiveCamStylingEnabled", () => {
expect(validateSettingKey("hpReactiveCamStylingEnabled")).toBe(
"hpReactiveCamStylingEnabled"
);
});
it("should throw TypeError for empty string", () => {
expect(() => validateSettingKey("")).toThrow(TypeError);
expect(() => validateSettingKey("")).toThrow(
"Setting key must be a non-empty string"
);
});
it("should throw TypeError for non-string", () => {
expect(() => validateSettingKey(123)).toThrow(TypeError);
expect(() => validateSettingKey(123)).toThrow(
"Setting key must be a non-empty string"
);
});
it("should throw TypeError for unknown key", () => {
expect(() => validateSettingKey("unknownKey")).toThrow(TypeError);
expect(() => validateSettingKey("unknownKey")).toThrow("unknown key");
});
it("should throw TypeError for null", () => {
expect(() => validateSettingKey(null)).toThrow(TypeError);
expect(() => validateSettingKey(null)).toThrow(
"Setting key must be a non-empty string"
);
});
it("should throw TypeError for undefined", () => {
expect(() => validateSettingKey(undefined)).toThrow(TypeError);
expect(() => validateSettingKey(undefined)).toThrow(
"Setting key must be a non-empty string"
);
});
});
describe("validateSettingValue", () => {
it("should accept true", () => {
expect(validateSettingValue(true)).toBe(true);
});
it("should accept false", () => {
expect(validateSettingValue(false)).toBe(false);
});
it("should throw TypeError for string", () => {
expect(() => validateSettingValue("true")).toThrow(TypeError);
expect(() => validateSettingValue("true")).toThrow(
"value must be a boolean"
);
});
it("should throw TypeError for number", () => {
expect(() => validateSettingValue(1)).toThrow(TypeError);
expect(() => validateSettingValue(1)).toThrow(
"value must be a boolean"
);
});
it("should throw TypeError for null", () => {
expect(() => validateSettingValue(null)).toThrow(TypeError);
expect(() => validateSettingValue(null)).toThrow(
"value must be a boolean"
);
});
it("should throw TypeError for undefined", () => {
expect(() => validateSettingValue(undefined)).toThrow(TypeError);
expect(() => validateSettingValue(undefined)).toThrow(
"value must be a boolean"
);
});
it("should throw TypeError for object", () => {
expect(() => validateSettingValue({})).toThrow(TypeError);
expect(() => validateSettingValue({})).toThrow(
"value must be a boolean"
);
});
});
describe("validateFeatureName", () => {
it("should accept valid feature: reactionCam", () => {
expect(validateFeatureName("reactionCam")).toBe("reactionCam");
});
it("should accept valid feature: hpReactiveCamStyling", () => {
expect(validateFeatureName("hpReactiveCamStyling")).toBe(
"hpReactiveCamStyling"
);
});
it("should throw TypeError for empty string", () => {
expect(() => validateFeatureName("")).toThrow(TypeError);
expect(() => validateFeatureName("")).toThrow(
"Feature name must be a non-empty string"
);
});
it("should throw TypeError for non-string", () => {
expect(() => validateFeatureName(123)).toThrow(TypeError);
expect(() => validateFeatureName(123)).toThrow(
"Feature name must be a non-empty string"
);
});
it("should throw TypeError for unknown feature", () => {
expect(() => validateFeatureName("unknownFeature")).toThrow(TypeError);
expect(() => validateFeatureName("unknownFeature")).toThrow(
"unknown feature"
);
});
it("should throw TypeError for null", () => {
expect(() => validateFeatureName(null)).toThrow(TypeError);
expect(() => validateFeatureName(null)).toThrow(
"Feature name must be a non-empty string"
);
});
it("should throw TypeError for undefined", () => {
expect(() => validateFeatureName(undefined)).toThrow(TypeError);
expect(() => validateFeatureName(undefined)).toThrow(
"Feature name must be a non-empty string"
);
});
});
// ==================== PORTRAIT FALLBACK TESTS ====================
describe("portrait fallback constants", () => {
it("should export MAX_PORTRAIT_SIZE as 5MB", () => {
expect(MAX_PORTRAIT_SIZE).toBe(5 * 1024 * 1024);
});
it("should export VALID_PORTRAIT_FORMATS with supported MIME types", () => {
expect(VALID_PORTRAIT_FORMATS).toEqual([
"image/png",
"image/jpeg",
"image/webp",
"image/gif",
]);
});
});
describe("updated PRIVACY_SETTINGS_DEFAULT with portrait fallback", () => {
it("should include customPortraitFallback key with null default", () => {
expect(PRIVACY_SETTINGS_DEFAULT).toHaveProperty("customPortraitFallback");
expect(PRIVACY_SETTINGS_DEFAULT.customPortraitFallback).toBeNull();
});
it("should retain existing boolean settings", () => {
expect(PRIVACY_SETTINGS_DEFAULT.reactionCamEnabled).toBe(false);
expect(PRIVACY_SETTINGS_DEFAULT.hpReactiveCamStylingEnabled).toBe(false);
});
});
describe("updated PRIVACY_SETTING_KEYS with portrait fallback", () => {
it("should include customPortraitFallback in keys", () => {
expect(PRIVACY_SETTING_KEYS).toContain("customPortraitFallback");
});
it("should retain existing keys", () => {
expect(PRIVACY_SETTING_KEYS).toContain("reactionCamEnabled");
expect(PRIVACY_SETTING_KEYS).toContain("hpReactiveCamStylingEnabled");
});
});
describe("updated createPrivacySettings with portrait fallback", () => {
it("should include customPortraitFallback in result when not overridden", () => {
const result = createPrivacySettings();
expect(result).toHaveProperty("customPortraitFallback");
expect(result.customPortraitFallback).toBeNull();
});
it("should allow customPortraitFallback to be overridden", () => {
const dataURL = "data:image/png;base64,test";
const result = createPrivacySettings({ customPortraitFallback: dataURL });
expect(result.customPortraitFallback).toBe(dataURL);
});
it("should ignore extra properties not in PRIVACY_SETTING_KEYS", () => {
const result = createPrivacySettings({
reactionCamEnabled: true,
customPortraitFallback: "data:image/png;base64,test",
unknownProp: "should be ignored",
});
expect(result).not.toHaveProperty("unknownProp");
expect(result.reactionCamEnabled).toBe(true);
expect(result.customPortraitFallback).toBe("data:image/png;base64,test");
});
});
describe("updated isValidPrivacySettings with portrait fallback", () => {
it("should accept valid settings with customPortraitFallback as string", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: "data:image/png;base64,test",
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
it("should accept valid settings with customPortraitFallback as null", () => {
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
};
expect(isValidPrivacySettings(valid)).toEqual(valid);
});
it("should accept valid settings without customPortraitFallback key", () => {
// Backward compatibility - may not have the key
const valid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
};
// This should still work - null/undefined is acceptable
expect(() => isValidPrivacySettings(valid)).not.toThrow();
});
it("should throw TypeError when customPortraitFallback is not string or null", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: 123,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"customPortraitFallback must be a string or null"
);
});
it("should throw TypeError when customPortraitFallback is a boolean", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: true,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"customPortraitFallback must be a string or null"
);
});
it("should throw TypeError when customPortraitFallback is an object", () => {
const invalid = {
reactionCamEnabled: false,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: {},
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"customPortraitFallback must be a string or null"
);
});
});
describe("validatePortraitDataURL", () => {
it("should accept valid PNG DataURL", () => {
const dataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
expect(validatePortraitDataURL(dataURL)).toBe(dataURL);
});
it("should accept valid JPEG DataURL", () => {
const dataURL = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAP//";
expect(validatePortraitDataURL(dataURL)).toBe(dataURL);
});
it("should accept valid WEBP DataURL", () => {
const dataURL = "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=";
expect(validatePortraitDataURL(dataURL)).toBe(dataURL);
});
it("should accept valid GIF DataURL", () => {
const dataURL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAkQBADs=";
expect(validatePortraitDataURL(dataURL)).toBe(dataURL);
});
it("should accept null as valid (no custom portrait)", () => {
expect(validatePortraitDataURL(null)).toBeNull();
});
it("should accept undefined as valid (no custom portrait)", () => {
expect(validatePortraitDataURL(undefined)).toBeUndefined();
});
it("should accept empty string as valid (explicitly no portrait)", () => {
expect(validatePortraitDataURL("")).toBe("");
});
it("should throw TypeError for invalid MIME type SVG", () => {
const dataURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==";
expect(() => validatePortraitDataURL(dataURL)).toThrow(TypeError);
expect(() => validatePortraitDataURL(dataURL)).toThrow("Unsupported portrait format");
});
it("should throw TypeError for invalid MIME type MP4", () => {
const dataURL = "data:video/mp4;base64,AAAAIGZ0eXBhdmlmAAAA";
expect(() => validatePortraitDataURL(dataURL)).toThrow(TypeError);
expect(() => validatePortraitDataURL(dataURL)).toThrow("Invalid DataURL format");
});
it("should throw TypeError for malformed DataURL without MIME type", () => {
const dataURL = "data:,test";
expect(() => validatePortraitDataURL(dataURL)).toThrow(TypeError);
expect(() => validatePortraitDataURL(dataURL)).toThrow("Invalid DataURL format");
});
it("should throw TypeError for non-DataURL string", () => {
const notDataURL = "https://example.com/image.png";
expect(() => validatePortraitDataURL(notDataURL)).toThrow(TypeError);
expect(() => validatePortraitDataURL(notDataURL)).toThrow("Invalid DataURL format");
});
it("should throw TypeError for non-string value", () => {
expect(() => validatePortraitDataURL(123)).toThrow(TypeError);
expect(() => validatePortraitDataURL(123)).toThrow("Invalid DataURL");
});
});
});