4 changed files with 171 additions and 7 deletions
-
12src/components/Componentes/question-answer-storage.tsx
-
1src/components/Componentes/slider-page.test.tsx
-
2src/components/Componentes/test-questions-flow.tsx
-
163src/lib/user-scoped-storage.test.ts
@ -0,0 +1,163 @@ |
|||||
|
import { describe, expect, it, beforeEach } from "vitest"; |
||||
|
import { |
||||
|
SCOPED_STORAGE_VERSION, |
||||
|
getScopedSectionDraftKey, |
||||
|
getScopedAssessmentDraftKey, |
||||
|
readScopedSectionDraft, |
||||
|
readScopedAssessmentDraft, |
||||
|
writeScopedSectionDraft, |
||||
|
writeScopedAssessmentDraft, |
||||
|
removeScopedSectionDraft, |
||||
|
removeScopedAssessmentDraft, |
||||
|
migrateLegacyStorageKeys, |
||||
|
isLegacyMigrationDone, |
||||
|
isDraftOwnedBy, |
||||
|
getAllScopedSectionDraftKeys, |
||||
|
extractSlugFromScopedKey, |
||||
|
} from "./user-scoped-storage"; |
||||
|
|
||||
|
describe("user-scoped-storage", () => { |
||||
|
beforeEach(() => { |
||||
|
window.localStorage.clear(); |
||||
|
}); |
||||
|
|
||||
|
describe("Key generators", () => { |
||||
|
it("generates profile-scoped section draft keys with version", () => { |
||||
|
const key = getScopedSectionDraftKey(42, "personal_info"); |
||||
|
expect(key).toBe(`marriage:user:42:sections:personal_info:draft:v${SCOPED_STORAGE_VERSION}`); |
||||
|
}); |
||||
|
|
||||
|
it("generates profile-scoped assessment draft keys with version", () => { |
||||
|
const key = getScopedAssessmentDraftKey(42, "personality_test"); |
||||
|
expect(key).toBe(`marriage:user:42:tests:personality_test:draft:v${SCOPED_STORAGE_VERSION}`); |
||||
|
}); |
||||
|
|
||||
|
it("extracts slug correctly from scoped key", () => { |
||||
|
const key = getScopedSectionDraftKey(101, "family_background"); |
||||
|
expect(extractSlugFromScopedKey(key)).toBe("family_background"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Section draft isolation across accounts", () => { |
||||
|
it("isolates Account A edits from Account B", () => { |
||||
|
const accountAProfileId = 1001; |
||||
|
const accountBProfileId = 2002; |
||||
|
|
||||
|
// Account A writes a draft
|
||||
|
writeScopedSectionDraft(accountAProfileId, "job_info", { |
||||
|
job_title: { |
||||
|
key: "job_title", |
||||
|
label: "Job Title", |
||||
|
type: "text", |
||||
|
value: "Engineer", |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
// Account A can read it
|
||||
|
const draftA = readScopedSectionDraft(accountAProfileId, "job_info"); |
||||
|
expect(draftA).not.toBeNull(); |
||||
|
expect(draftA?.pending.job_title.value).toBe("Engineer"); |
||||
|
expect(draftA?.ownerProfileId).toBe(accountAProfileId); |
||||
|
|
||||
|
// Account B CANNOT read Account A's draft
|
||||
|
const draftB = readScopedSectionDraft(accountBProfileId, "job_info"); |
||||
|
expect(draftB).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("removes draft when pending is empty", () => { |
||||
|
const profileId = 123; |
||||
|
writeScopedSectionDraft(profileId, "bio", { |
||||
|
about: { key: "about", label: "About", type: "text", value: "Hello" }, |
||||
|
}); |
||||
|
expect(readScopedSectionDraft(profileId, "bio")).not.toBeNull(); |
||||
|
|
||||
|
// Write empty pending map -> draft is removed
|
||||
|
writeScopedSectionDraft(profileId, "bio", {}); |
||||
|
expect(readScopedSectionDraft(profileId, "bio")).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("removes draft explicitly via removeScopedSectionDraft", () => { |
||||
|
const profileId = 123; |
||||
|
writeScopedSectionDraft(profileId, "bio", { |
||||
|
about: { key: "about", label: "About", type: "text", value: "Hello" }, |
||||
|
}); |
||||
|
removeScopedSectionDraft(profileId, "bio"); |
||||
|
expect(readScopedSectionDraft(profileId, "bio")).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("lists all scoped draft keys for a given profile", () => { |
||||
|
writeScopedSectionDraft(1, "sec_1", { q: { key: "q", label: "Q", type: "text", value: "A" } }); |
||||
|
writeScopedSectionDraft(1, "sec_2", { q: { key: "q", label: "Q", type: "text", value: "B" } }); |
||||
|
writeScopedSectionDraft(2, "sec_3", { q: { key: "q", label: "Q", type: "text", value: "C" } }); |
||||
|
|
||||
|
const keys1 = getAllScopedSectionDraftKeys(1); |
||||
|
expect(keys1).toHaveLength(2); |
||||
|
expect(keys1.map(extractSlugFromScopedKey)).toEqual(expect.arrayContaining(["sec_1", "sec_2"])); |
||||
|
|
||||
|
const keys2 = getAllScopedSectionDraftKeys(2); |
||||
|
expect(keys2).toHaveLength(1); |
||||
|
expect(extractSlugFromScopedKey(keys2[0])).toBe("sec_3"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Assessment draft isolation across accounts", () => { |
||||
|
it("isolates assessment answers across accounts", () => { |
||||
|
const userA = 501; |
||||
|
const userB = 502; |
||||
|
|
||||
|
writeScopedAssessmentDraft(userA, "personality_test", { 1: "A", 2: "B" }, 2, 10); |
||||
|
|
||||
|
const draftA = readScopedAssessmentDraft(userA, "personality_test"); |
||||
|
expect(draftA).not.toBeNull(); |
||||
|
expect(draftA?.answers).toEqual({ 1: "A", 2: "B" }); |
||||
|
expect(draftA?.currentIndex).toBe(2); |
||||
|
expect(draftA?.totalQuestions).toBe(10); |
||||
|
|
||||
|
const draftB = readScopedAssessmentDraft(userB, "personality_test"); |
||||
|
expect(draftB).toBeNull(); |
||||
|
}); |
||||
|
|
||||
|
it("removes assessment draft explicitly", () => { |
||||
|
writeScopedAssessmentDraft(100, "glasser_5_needs_test", { 1: 5 }, 1, 20); |
||||
|
removeScopedAssessmentDraft(100, "glasser_5_needs_test"); |
||||
|
expect(readScopedAssessmentDraft(100, "glasser_5_needs_test")).toBeNull(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Legacy migration", () => { |
||||
|
it("removes old unscoped keys while preserving unrelated localStorage", () => { |
||||
|
// Setup old keys
|
||||
|
window.localStorage.setItem("marriage:sections:about:answers:v2", JSON.stringify({ fields: [] })); |
||||
|
window.localStorage.setItem("marriage:tests:personality_test:draft", JSON.stringify({ answers: { 1: "A" } })); |
||||
|
window.localStorage.setItem("marriage:sections:glasser_5_needs_test:answers", JSON.stringify({ completed: true })); |
||||
|
|
||||
|
// Setup unrelated keys that MUST NOT be touched
|
||||
|
window.localStorage.setItem("marriage:device_id", "device-12345"); |
||||
|
window.localStorage.setItem("theme_preference", "dark"); |
||||
|
|
||||
|
expect(isLegacyMigrationDone()).toBe(false); |
||||
|
|
||||
|
migrateLegacyStorageKeys(); |
||||
|
|
||||
|
expect(isLegacyMigrationDone()).toBe(true); |
||||
|
|
||||
|
// Old keys should be gone
|
||||
|
expect(window.localStorage.getItem("marriage:sections:about:answers:v2")).toBeNull(); |
||||
|
expect(window.localStorage.getItem("marriage:tests:personality_test:draft")).toBeNull(); |
||||
|
expect(window.localStorage.getItem("marriage:sections:glasser_5_needs_test:answers")).toBeNull(); |
||||
|
|
||||
|
// Unrelated keys must be preserved
|
||||
|
expect(window.localStorage.getItem("marriage:device_id")).toBe("device-12345"); |
||||
|
expect(window.localStorage.getItem("theme_preference")).toBe("dark"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Ownership verification helper", () => { |
||||
|
it("verifies draft ownership correctly", () => { |
||||
|
expect(isDraftOwnedBy({ ownerProfileId: 99 }, 99)).toBe(true); |
||||
|
expect(isDraftOwnedBy({ ownerProfileId: 99 }, 100)).toBe(false); |
||||
|
expect(isDraftOwnedBy(null, 99)).toBe(false); |
||||
|
expect(isDraftOwnedBy(undefined, 99)).toBe(false); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue