Files
Joel Salmon 33674f92f4 Complete Phase 1: goals, cookie auth, profile editing
Close the remaining Phase 1 DoD gaps and reconcile the browser flow with
the auth layer.

Goals (5 -> 7 prompts):
- Add near-term (6-12mo) and long-term (3-5yr) goal prompts; collect raw
  text on the conversation and store AI-articulated goal summaries on the
  profile. Extractor articulates the person's own stated goals (mirror,
  not compass) and never fabricates. Alembic 003 adds the four columns.

Cookie-based browser sessions (fixes frontend<->auth desync):
- OAuth callback now sets httpOnly session cookies and redirects into the
  app instead of returning JSON. get_current_user gains a cookie fallback
  (X-API-Key -> Bearer -> cookie). refresh/logout read the refresh cookie
  and set/clear cookies. New shared auth.js (authedFetch) sends cookies and
  silently refreshes on 401. Static pages drop the bogus user_id and call
  the correct /me endpoints.

Profile editing (read/edit/affirm):
- PATCH /discovery/profile/me edits the prose (Ikigai summaries, overlap
  narrative, goals); owner-scoped, partial update, 409 when locked. Edit
  mode in profile.html with Save/Cancel.

Also: bump default model to claude-sonnet-4-6, align ports to 8011
(OAuth redirect, CORS), add COOKIE_SECURE/POST_LOGIN_REDIRECT config, and
refresh the README to match the shipped behavior.

Tests: 33 passing (added cookie-auth, profile-edit, goal-extraction cases;
factored a shared app_client fixture into conftest.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:26:08 -05:00

113 lines
3.4 KiB
Python

"""Tests for PATCH /discovery/profile/me (profile editing).
These run under the X-API-Key admin identity, editing a profile seeded for
that user, so they don't depend on the Google OAuth flow.
"""
import uuid
from datetime import datetime, timezone
API_KEY = {"X-API-Key": "test-api-key"}
async def _seed_profile(locked: bool = False) -> str:
"""Insert a conversation + profile for the API-key admin user."""
from app.auth import API_KEY_ADMIN_ID, ensure_api_key_admin
from app.database import AsyncSessionLocal
from app.models import DiscoveryConversation, DiscoveryProfile
async with AsyncSessionLocal() as db:
await ensure_api_key_admin(db)
conv = DiscoveryConversation(
id=str(uuid.uuid4()),
user_id=API_KEY_ADMIN_ID,
started_at=datetime.now(timezone.utc),
)
db.add(conv)
await db.commit()
profile = DiscoveryProfile(
id=str(uuid.uuid4()),
user_id=API_KEY_ADMIN_ID,
conversation_id=conv.id,
generated_at=datetime.now(timezone.utc),
triad="gut",
probable_type=8,
wing=9,
instinctual_variant="sp",
love_summary="orig love",
strength_summary="orig strength",
mission_summary="orig mission",
vocation_summary="orig vocation",
overlap_narrative="orig narrative",
short_term_goals="orig short",
long_term_goals="orig long",
locked=locked,
)
db.add(profile)
await db.commit()
return profile.id
async def test_patch_updates_prose(app_client):
await _seed_profile()
r = await app_client.patch(
"/discovery/profile/me",
headers=API_KEY,
json={"love_summary": "new love", "overlap_narrative": "new narrative"},
)
assert r.status_code == 200
body = r.json()
assert body["love_summary"] == "new love"
assert body["overlap_narrative"] == "new narrative"
# Untouched prose preserved; structural inference is never editable here.
assert body["strength_summary"] == "orig strength"
assert body["triad"] == "gut"
assert body["probable_type"] == 8
async def test_patch_partial_does_not_clear_other_fields(app_client):
await _seed_profile()
r = await app_client.patch(
"/discovery/profile/me",
headers=API_KEY,
json={"short_term_goals": "updated goal"},
)
assert r.status_code == 200
body = r.json()
assert body["short_term_goals"] == "updated goal"
assert body["long_term_goals"] == "orig long"
async def test_patch_locked_profile_returns_409(app_client):
await _seed_profile(locked=True)
r = await app_client.patch(
"/discovery/profile/me",
headers=API_KEY,
json={"love_summary": "x"},
)
assert r.status_code == 409
async def test_patch_without_profile_returns_404(app_client):
r = await app_client.patch(
"/discovery/profile/me",
headers=API_KEY,
json={"love_summary": "x"},
)
assert r.status_code == 404
async def test_patch_empty_payload_returns_400(app_client):
await _seed_profile()
r = await app_client.patch(
"/discovery/profile/me", headers=API_KEY, json={}
)
assert r.status_code == 400
async def test_patch_requires_auth(app_client):
r = await app_client.patch(
"/discovery/profile/me", json={"love_summary": "x"}
)
assert r.status_code == 401