-
-
Notifications
You must be signed in to change notification settings - Fork 4
Blog and Sanity: fixes and refactoring #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 40 commits
725d234
6e8561d
0334ea5
7bf3768
d60fbe8
c836b11
705f075
69a8190
f628c8c
a2fa0b6
42d1a62
faab3bd
102f427
91f05d8
d73087a
9347874
2591e67
a04e8bb
83d54a6
f0a1419
fd582c2
f0144a3
556e400
7e54349
177a77e
ded4abf
9faffa5
7299946
1f293ac
6d7fffa
cafee1b
74bb624
5f907e4
a64eecd
5a6ad1e
dc2ab9c
8c081f4
7e828be
76083b6
d7355b7
fc386d3
e113491
d80e555
df07575
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { | ||
| createEncryptedAnswersBlob, | ||
| decryptAnswers, | ||
| encryptAnswers, | ||
| } from '@/lib/quiz/quiz-crypto'; | ||
|
|
||
| import { | ||
| createCorrectAnswersMap, | ||
| createMockQuestions, | ||
| resetFactoryCounters, | ||
| } from '../factories/quiz/quiz'; | ||
| import { cleanupQuizTestEnv, setupQuizTestEnv } from './setup'; | ||
|
|
||
| describe('quiz-crypto', () => { | ||
| // Setup: set encryption key before each test | ||
| beforeEach(() => { | ||
| setupQuizTestEnv(); | ||
| resetFactoryCounters(); | ||
| }); | ||
|
|
||
| // Cleanup: remove encryption key after each test | ||
| afterEach(() => { | ||
| cleanupQuizTestEnv(); | ||
| }); | ||
|
|
||
| describe('encryptAnswers', () => { | ||
| it('returns a base64 string', () => { | ||
| const answers = { 'q-1': 'a-1' }; | ||
|
|
||
| const result = encryptAnswers(answers); | ||
|
|
||
| // Base64 pattern: letters, numbers, +, /, ends with optional = | ||
| expect(result).toMatch(/^[A-Za-z0-9+/]+=*$/); | ||
| }); | ||
|
|
||
| it('returns different output for same input (random IV)', () => { | ||
| const answers = { 'q-1': 'a-1' }; | ||
|
|
||
| const result1 = encryptAnswers(answers); | ||
| const result2 = encryptAnswers(answers); | ||
|
|
||
| // Each encryption uses random IV, so outputs differ | ||
| expect(result1).not.toBe(result2); | ||
| }); | ||
|
|
||
| it('handles empty object', () => { | ||
| const result = encryptAnswers({}); | ||
|
|
||
| expect(result).toBeDefined(); | ||
| expect(typeof result).toBe('string'); | ||
| }); | ||
|
|
||
| it('handles multiple questions', () => { | ||
| const answers = { | ||
| 'q-1': 'a-1', | ||
| 'q-2': 'a-2', | ||
| 'q-3': 'a-3', | ||
| }; | ||
|
|
||
| const result = encryptAnswers(answers); | ||
|
|
||
| expect(result).toBeDefined(); | ||
| expect(result.length).toBeGreaterThan(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('decryptAnswers', () => { | ||
| it('decrypts back to original data', () => { | ||
| const original = { 'q-1': 'a-1', 'q-2': 'a-2' }; | ||
|
|
||
| const encrypted = encryptAnswers(original); | ||
| const decrypted = decryptAnswers(encrypted); | ||
|
|
||
| expect(decrypted).toEqual(original); | ||
| }); | ||
|
|
||
| it('returns null for tampered data', () => { | ||
| const original = { 'q-1': 'a-1' }; | ||
| const encrypted = encryptAnswers(original); | ||
|
|
||
| // Tamper: change last 5 characters | ||
| const tampered = encrypted.slice(0, -5) + 'XXXXX'; | ||
|
|
||
| const result = decryptAnswers(tampered); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for invalid base64', () => { | ||
| const result = decryptAnswers('not-valid-base64!!!'); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for empty string', () => { | ||
| const result = decryptAnswers(''); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns null for truncated data', () => { | ||
| const original = { 'q-1': 'a-1' }; | ||
| const encrypted = encryptAnswers(original); | ||
|
|
||
| // Truncate: remove half of the data | ||
| const truncated = encrypted.slice(0, encrypted.length / 2); | ||
|
|
||
| const result = decryptAnswers(truncated); | ||
|
|
||
| expect(result).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('createEncryptedAnswersBlob', () => { | ||
| it('creates encrypted blob from questions', () => { | ||
| const questions = createMockQuestions(3); | ||
|
|
||
| const blob = createEncryptedAnswersBlob(questions); | ||
|
|
||
| expect(blob).toBeDefined(); | ||
| expect(typeof blob).toBe('string'); | ||
| expect(blob.length).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it('encrypted blob decrypts to correct answers map', () => { | ||
| const questions = createMockQuestions(3); | ||
| const expectedMap = createCorrectAnswersMap(questions); | ||
|
|
||
| const blob = createEncryptedAnswersBlob(questions); | ||
| const decrypted = decryptAnswers(blob); | ||
|
|
||
| expect(decrypted).toEqual(expectedMap); | ||
| }); | ||
|
|
||
| it('handles questions with no correct answer', () => { | ||
| const questions = [ | ||
| { | ||
| id: 'q-no-correct', | ||
| answers: [ | ||
| { id: 'a-1', isCorrect: false }, | ||
| { id: 'a-2', isCorrect: false }, | ||
| ], | ||
| }, | ||
| ]; | ||
|
|
||
| const blob = createEncryptedAnswersBlob(questions); | ||
| const decrypted = decryptAnswers(blob); | ||
|
|
||
| // Question with no correct answer is not included in map | ||
| expect(decrypted).toEqual({}); | ||
| }); | ||
|
|
||
| it('handles empty questions array', () => { | ||
| const blob = createEncryptedAnswersBlob([]); | ||
| const decrypted = decryptAnswers(blob); | ||
|
|
||
| expect(decrypted).toEqual({}); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,8 @@ | ||||||||||||||||
| <<<<<<< HEAD | ||||||||||||||||
| import { afterEach, beforeEach, vi } from 'vitest'; | ||||||||||||||||
|
|
||||||||||||||||
| ======= | ||||||||||||||||
| >>>>>>> develop | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unresolved merge conflict blocks compilation. The file contains Git conflict markers ( 🐛 Proposed fix: Resolve conflict by keeping the vitest import-<<<<<<< HEAD
import { afterEach, beforeEach, vi } from 'vitest';
-
-=======
->>>>>>> develop
+📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.3.13)[error] 1-1: Expected a statement but instead found '<<<<<<< HEAD'. Expected a statement here. (parse) [error] 3-5: Expected a statement but instead found '=======
Expected a statement here. (parse) 🤖 Prompt for AI Agents |
||||||||||||||||
| /** | ||||||||||||||||
| * 32-byte hex key for AES-256 (64 hex characters) | ||||||||||||||||
| * Only used in tests — not a real secret | ||||||||||||||||
|
|
||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.