|
| 1 | +import { sealData, unsealData } from './seal'; |
| 2 | + |
| 3 | +describe('seal', () => { |
| 4 | + const password = 'test-password-at-least-32-characters-long'; |
| 5 | + |
| 6 | + describe('sealData', () => { |
| 7 | + it('seals data and appends version delimiter', async () => { |
| 8 | + const data = { foo: 'bar' }; |
| 9 | + const sealed = await sealData(data, { password }); |
| 10 | + |
| 11 | + expect(sealed).toContain('~2'); |
| 12 | + expect(sealed.startsWith('Fe26.2*')).toBe(true); |
| 13 | + }); |
| 14 | + }); |
| 15 | + |
| 16 | + describe('unsealData', () => { |
| 17 | + it('round-trips seal/unseal', async () => { |
| 18 | + const data = { userId: '123', email: '[email protected]' }; |
| 19 | + const sealed = await sealData(data, { password }); |
| 20 | + const unsealed = await unsealData<typeof data>(sealed, { password }); |
| 21 | + |
| 22 | + expect(unsealed).toEqual(data); |
| 23 | + }); |
| 24 | + |
| 25 | + it('round-trips complex nested data', async () => { |
| 26 | + const data = { |
| 27 | + user: { id: '123', name: 'Test' }, |
| 28 | + roles: ['admin', 'user'], |
| 29 | + metadata: { nested: { deep: true } }, |
| 30 | + }; |
| 31 | + const sealed = await sealData(data, { password }); |
| 32 | + const unsealed = await unsealData<typeof data>(sealed, { password }); |
| 33 | + |
| 34 | + expect(unsealed).toEqual(data); |
| 35 | + }); |
| 36 | + |
| 37 | + it('returns empty object for bad password', async () => { |
| 38 | + const data = { foo: 'bar' }; |
| 39 | + const sealed = await sealData(data, { password }); |
| 40 | + const unsealed = await unsealData(sealed, { |
| 41 | + password: 'wrong-password-at-least-32-characters', |
| 42 | + }); |
| 43 | + |
| 44 | + expect(unsealed).toEqual({}); |
| 45 | + }); |
| 46 | + |
| 47 | + it('returns empty object for tampered seal', async () => { |
| 48 | + const data = { foo: 'bar' }; |
| 49 | + const sealed = await sealData(data, { password }); |
| 50 | + // Tamper with the sealed data |
| 51 | + const tampered = sealed.replace('Fe26.2', 'Fe26.2tampered'); |
| 52 | + const unsealed = await unsealData(tampered, { password }); |
| 53 | + |
| 54 | + expect(unsealed).toEqual({}); |
| 55 | + }); |
| 56 | + |
| 57 | + it('handles legacy tokens with persistent field (version 1)', async () => { |
| 58 | + // Manually construct a v1-style token that would have persistent wrapper |
| 59 | + const data = { persistent: { userId: '123' } }; |
| 60 | + const sealed = await sealData(data, { password }); |
| 61 | + // Replace ~2 with ~1 to simulate v1 token |
| 62 | + const v1Sealed = sealed.replace('~2', '~1'); |
| 63 | + |
| 64 | + const unsealed = await unsealData<{ userId: string }>(v1Sealed, { |
| 65 | + password, |
| 66 | + }); |
| 67 | + expect(unsealed).toEqual({ userId: '123' }); |
| 68 | + }); |
| 69 | + |
| 70 | + it('handles tokens without version delimiter', async () => { |
| 71 | + const data = { foo: 'bar' }; |
| 72 | + const sealed = await sealData(data, { password }); |
| 73 | + // Remove version delimiter to simulate pre-versioned token |
| 74 | + const noVersion = sealed.split('~')[0]; |
| 75 | + |
| 76 | + const unsealed = await unsealData<typeof data>(noVersion, { password }); |
| 77 | + expect(unsealed).toEqual(data); |
| 78 | + }); |
| 79 | + }); |
| 80 | + |
| 81 | + describe('iron-session compatibility', () => { |
| 82 | + // This test verifies that tokens sealed with this implementation |
| 83 | + // produce the same format as iron-session (Fe26.2 prefix with ~2 suffix) |
| 84 | + it('produces iron-session compatible format', async () => { |
| 85 | + const data = { userId: '123' }; |
| 86 | + const sealed = await sealData(data, { password }); |
| 87 | + |
| 88 | + // Verify format: Fe26.2*...*~2 |
| 89 | + const parts = sealed.split('~'); |
| 90 | + expect(parts).toHaveLength(2); |
| 91 | + expect(parts[1]).toBe('2'); |
| 92 | + |
| 93 | + const ironPart = parts[0]; |
| 94 | + expect(ironPart.startsWith('Fe26.2*')).toBe(true); |
| 95 | + |
| 96 | + // Iron format has 8 asterisk-delimited components |
| 97 | + const components = ironPart.split('*'); |
| 98 | + expect(components).toHaveLength(8); |
| 99 | + expect(components[0]).toBe('Fe26.2'); |
| 100 | + }); |
| 101 | + |
| 102 | + // Verify that tokens can be round-tripped and maintain the expected format |
| 103 | + // This ensures compatibility with iron-session which uses the same Fe26.2 format |
| 104 | + it('can unseal self-generated tokens (simulates iron-session compatibility)', async () => { |
| 105 | + const data = { userId: '123', email: '[email protected]' }; |
| 106 | + const sealed = await sealData(data, { password }); |
| 107 | + |
| 108 | + // Verify the format matches iron-session's expected output |
| 109 | + expect(sealed).toMatch( |
| 110 | + /^Fe26\.2\*1\*[^*]+\*[^*]+\*[^*]+\*\*[^*]+\*[^*]+~2$/, |
| 111 | + ); |
| 112 | + |
| 113 | + const unsealed = await unsealData<typeof data>(sealed, { password }); |
| 114 | + expect(unsealed).toEqual(data); |
| 115 | + }); |
| 116 | + }); |
| 117 | +}); |
0 commit comments