497 lines
17 KiB
JavaScript
497 lines
17 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,
|
|
customPortraitFallback: null,
|
|
});
|
|
});
|
|
|
|
it("should export PRIVACY_SETTING_KEYS as frozen array including portrait fallback", () => {
|
|
expect(PRIVACY_SETTING_KEYS).toEqual([
|
|
"reactionCamEnabled",
|
|
"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",
|
|
});
|
|
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,
|
|
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,
|
|
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,
|
|
customPortraitFallback: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isValidPrivacySettings", () => {
|
|
it("should accept valid settings with all false", () => {
|
|
const valid = {
|
|
reactionCamEnabled: false,
|
|
};
|
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
|
});
|
|
|
|
it("should accept valid settings with all true", () => {
|
|
const valid = {
|
|
reactionCamEnabled: true,
|
|
};
|
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
|
});
|
|
|
|
it("should accept valid settings with mixed values", () => {
|
|
const valid = {
|
|
reactionCamEnabled: true,
|
|
};
|
|
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,
|
|
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",
|
|
};
|
|
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,
|
|
};
|
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
|
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
|
"reactionCamEnabled 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 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 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 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);
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
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,
|
|
customPortraitFallback: "data:image/png;base64,test",
|
|
};
|
|
expect(isValidPrivacySettings(valid)).toEqual(valid);
|
|
});
|
|
|
|
it("should accept valid settings with customPortraitFallback as null", () => {
|
|
const valid = {
|
|
reactionCamEnabled: 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,
|
|
};
|
|
// 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,
|
|
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,
|
|
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,
|
|
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");
|
|
});
|
|
});
|
|
});
|