Skip to content

Commit 4fd7ff8

Browse files
Merge pull request #200 from SaplingLearn/fix/schema-encryption-text-types
fix(db): type encryption columns as TEXT in canonical schema
2 parents 824d7e4 + 0af75ee commit 4fd7ff8

2 files changed

Lines changed: 40 additions & 4 deletions

File tree

backend/db/supabase_schema.sql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS sessions (
111111
course_id TEXT REFERENCES courses(id),
112112
started_at TIMESTAMPTZ DEFAULT now(),
113113
ended_at TIMESTAMPTZ,
114-
summary_json JSONB,
114+
summary_json TEXT, -- encrypted base64 text (AES-256-GCM via services/encryption.py)
115115
name TEXT
116116
);
117117

@@ -159,8 +159,8 @@ CREATE TABLE IF NOT EXISTS assignments (
159159
notes TEXT,
160160
google_event_id TEXT,
161161
category_id TEXT REFERENCES course_categories(id) ON DELETE SET NULL,
162-
points_possible NUMERIC,
163-
points_earned NUMERIC,
162+
points_possible TEXT, -- encrypted base64 text (AES-256-GCM via services/encryption.py)
163+
points_earned TEXT, -- encrypted base64 text (AES-256-GCM via services/encryption.py)
164164
source TEXT DEFAULT 'manual',
165165
created_at TIMESTAMPTZ DEFAULT now()
166166
);
@@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS documents (
176176
category TEXT NOT NULL,
177177
summary TEXT,
178178
flashcards JSONB,
179-
concept_notes JSONB,
179+
concept_notes TEXT, -- encrypted base64 text (AES-256-GCM via services/encryption.py)
180180
created_at TIMESTAMPTZ DEFAULT now(),
181181
processed_at TIMESTAMPTZ
182182
);

backend/tests/test_encryption.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,39 @@ def test_tampered_ciphertext_raises():
104104
tampered = base64.b64encode(bytes(raw)).decode()
105105
with pytest.raises(Exception):
106106
encryption.decrypt(tampered)
107+
108+
109+
# ── #200: columns retyped NUMERIC/JSONB -> TEXT in the canonical schema ─────────
110+
# These hold encrypted base64 *text*, so the value each column stores must survive
111+
# the exact write->read helpers the routes use. The route-level coverage for
112+
# documents.concept_notes lives in the test_documents_routes module quarantined
113+
# under #210, so assert the round-trip here (non-quarantined) before that change
114+
# lands. summary_json/concept_notes -> encrypt_json/decrypt_json; points ->
115+
# encrypt_if_present/decrypt_numeric (see routes/learn.py, routes/documents.py,
116+
# routes/gradebook.py).
117+
118+
def test_summary_json_text_column_round_trip():
119+
# sessions.summary_json — a JSON object (learn.py writes encrypt_json(summary))
120+
summary = {"concepts": ["limits", "derivatives"], "score": 0.82, "notes": "ünïcode"}
121+
assert encryption.decrypt_json(encryption.encrypt_json(summary)) == summary
122+
123+
124+
def test_concept_notes_text_column_round_trip():
125+
# documents.concept_notes — a JSON array of {name, description}
126+
# (documents.py writes encrypt_json(concept_notes))
127+
concept_notes = [
128+
{"name": "Eigenvalue", "description": "Scalar λ with Av = λv."},
129+
{"name": "Span", "description": "All linear combinations of a set."},
130+
]
131+
assert encryption.decrypt_json(encryption.encrypt_json(concept_notes)) == concept_notes
132+
133+
134+
def test_points_text_column_round_trip():
135+
# assignments.points_possible / points_earned — numbers written via
136+
# encrypt_if_present and read back via decrypt_numeric.
137+
for value in (100, 95.5, 0):
138+
ct = encryption.encrypt_if_present(value)
139+
assert isinstance(ct, str) # stored as TEXT, not NUMERIC
140+
assert encryption.decrypt_numeric(ct) == float(value)
141+
assert encryption.encrypt_if_present(None) is None
142+
assert encryption.decrypt_numeric(None) is None

0 commit comments

Comments
 (0)