Story 4.2 completed

This commit is contained in:
2026-05-24 00:37:21 +02:00
parent de1b33c453
commit 56eeb7cc83
21 changed files with 3836 additions and 56 deletions
+232 -19
View File
@@ -8,11 +8,14 @@ import {
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";
@@ -23,17 +26,19 @@ describe("privacy-settings contract", () => {
expect(PRIVACY_SETTINGS_VERSION).toBe(1);
});
it("should export PRIVACY_SETTINGS_DEFAULT with all false", () => {
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", () => {
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);
});
@@ -58,10 +63,11 @@ describe("privacy-settings contract", () => {
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
it("should allow both settings to be overridden", () => {
it("should allow both boolean settings to be overridden", () => {
const result = createPrivacySettings({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: true,
@@ -69,6 +75,17 @@ describe("privacy-settings contract", () => {
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,
});
});
@@ -86,6 +103,7 @@ describe("privacy-settings contract", () => {
expect(result).toEqual({
reactionCamEnabled: true,
hpReactiveCamStylingEnabled: false,
customPortraitFallback: null,
});
});
});
@@ -197,24 +215,25 @@ describe("privacy-settings contract", () => {
);
});
it("should throw TypeError for missing reactionCamEnabled", () => {
const invalid = {
hpReactiveCamStylingEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"reactionCamEnabled must be a boolean"
);
});
it("should throw TypeError for missing hpReactiveCamStylingEnabled", () => {
const invalid = {
it("should accept settings with only reactionCamEnabled (backward compatible)", () => {
// Backward compatibility: settings without all keys are accepted
const valid = {
reactionCamEnabled: false,
};
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
expect(() => isValidPrivacySettings(invalid)).toThrow(
"hpReactiveCamStylingEnabled must be a boolean"
);
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();
});
});
@@ -354,4 +373,198 @@ describe("privacy-settings contract", () => {
);
});
});
// ==================== 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");
});
});
});