61f362004e
- Created src/contracts/privacy-settings.js with: - PrivacySettings typedef - PRIVACY_SETTINGS_DEFAULT (both flags false) - PRIVACY_SETTING_KEYS and FEATURE_NAME_MAP constants - createPrivacySettings() factory - isValidPrivacySettings() validator - validateSettingKey(), validateSettingValue(), validateFeatureName() helpers - Created src/core/PlayerPrivacyManager.js with: - Constructor with FoundryAdapter DI validation - getSettings(userId) - retrieves settings from user flags - setSetting(userId, key, value) - async, validates, persists via user.setFlag - isOptedIn(userId, feature) - convenience method for feature checks - getAllSettings() - aggregates all users' settings (GM view) - onChange(callback) - subscription pattern for change events - teardown() - cleanup - Created tests/unit/contracts/privacy-settings.test.js - 44 tests - Created tests/unit/core/PlayerPrivacyManager.test.js - 35 tests - All tests passing, lint clean - Updated sprint-status.yaml: 4-1 from ready-for-dev to in-progress - Updated story file: Task 1 subtasks 1.1-1.8 marked complete Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
358 lines
12 KiB
JavaScript
358 lines
12 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,
|
|
createPrivacySettings,
|
|
isValidPrivacySettings,
|
|
validateSettingKey,
|
|
validateSettingValue,
|
|
validateFeatureName,
|
|
} 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", () => {
|
|
expect(PRIVACY_SETTINGS_DEFAULT).toEqual({
|
|
reactionCamEnabled: false,
|
|
hpReactiveCamStylingEnabled: false,
|
|
});
|
|
});
|
|
|
|
it("should export PRIVACY_SETTING_KEYS as frozen array", () => {
|
|
expect(PRIVACY_SETTING_KEYS).toEqual([
|
|
"reactionCamEnabled",
|
|
"hpReactiveCamStylingEnabled",
|
|
]);
|
|
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,
|
|
});
|
|
});
|
|
|
|
it("should allow both settings to be overridden", () => {
|
|
const result = createPrivacySettings({
|
|
reactionCamEnabled: true,
|
|
hpReactiveCamStylingEnabled: true,
|
|
});
|
|
expect(result).toEqual({
|
|
reactionCamEnabled: true,
|
|
hpReactiveCamStylingEnabled: true,
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
|
|
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 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 = {
|
|
reactionCamEnabled: false,
|
|
};
|
|
expect(() => isValidPrivacySettings(invalid)).toThrow(TypeError);
|
|
expect(() => isValidPrivacySettings(invalid)).toThrow(
|
|
"hpReactiveCamStylingEnabled must be a boolean"
|
|
);
|
|
});
|
|
});
|
|
|
|
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"
|
|
);
|
|
});
|
|
});
|
|
});
|