/** * 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"); }); }); });