1- import pytest
2- import subprocess
3- import time
41import os
52import socket
6- from dotenv import load_dotenv
3+ import subprocess
4+ import time
5+ from contextlib import contextmanager
6+
7+ import pytest
8+ from playwright .sync_api import Browser , Page
9+ from tests .browser .db_helpers import browser_db_env
710from utils .core .db import (
11+ ensure_database_exists ,
812 get_connection_url ,
913 set_up_db ,
1014 tear_down_db ,
11- ensure_database_exists ,
1215)
1316
1417
@@ -17,49 +20,145 @@ def _port_free(port: int) -> bool:
1720 return s .connect_ex (("127.0.0.1" , port )) != 0
1821
1922
20- @pytest .fixture (scope = "session" )
21- def browser_env ():
22- """Build an environment dict for the live server subprocess."""
23- load_dotenv ()
24- env = os .environ .copy ()
25- env ["DB_NAME" ] = "webapp-browser-test-db"
26- env ["SECRET_KEY" ] = "testsecretkey-that-is-at-least-32-bytes-long"
27- env ["HOST_NAME" ] = "Test Organization"
28- env ["RESEND_API_KEY" ] = "test"
29- env ["EMAIL_FROM" ] = "test@example.com"
30- env ["BASE_URL" ] = "http://127.0.0.1:8113"
31- return env
23+ @contextmanager
24+ def _temporary_env (env : dict [str , str ]):
25+ """Apply env vars for DB helpers without leaking into the pytest process."""
26+ saved = {key : os .environ .get (key ) for key in env }
27+ os .environ .update (env )
28+ try :
29+ yield
30+ finally :
31+ for key , value in saved .items ():
32+ if value is None :
33+ os .environ .pop (key , None )
34+ else :
35+ os .environ [key ] = value
3236
3337
34- @pytest .fixture (scope = "session" )
35- def live_server (browser_env ):
36- """Start a uvicorn server for Playwright tests and return the base URL."""
37- # Apply env so our DB helpers use the right database
38- os .environ .update (browser_env )
38+ def register_user (
39+ browser : Browser ,
40+ live_server : str ,
41+ * ,
42+ name : str ,
43+ email : str ,
44+ password : str ,
45+ ) -> None :
46+ """Register a user through the live server UI (session-scoped helper)."""
47+ context = browser .new_context (viewport = {"width" : 1280 , "height" : 720 })
48+ page = context .new_page ()
49+ page .goto (f"{ live_server } /account/register" )
50+ page .fill ("#name" , name )
51+ page .fill ("#email" , email )
52+ page .fill ("#password" , password )
53+ page .fill ("#confirm_password" , password )
54+ page .click ('button[type="submit"]' )
55+ page .wait_for_function (
56+ "window.location.pathname.startsWith('/dashboard')" , timeout = 10_000
57+ )
58+ context .close ()
59+
3960
40- ensure_database_exists (get_connection_url ())
41- set_up_db (drop = True )
61+ def login_user (
62+ browser : Browser ,
63+ live_server : str ,
64+ * ,
65+ email : str ,
66+ password : str ,
67+ ) -> Page :
68+ """Log in via the live server and return a page on the dashboard."""
69+ context = browser .new_context (viewport = {"width" : 1280 , "height" : 720 })
70+ page = context .new_page ()
71+ page .goto (f"{ live_server } /account/login" )
72+ page .fill ("#email" , email )
73+ page .fill ("#password" , password )
74+ page .click ('button[type="submit"]' )
75+ page .wait_for_function (
76+ "window.location.pathname.startsWith('/dashboard')" , timeout = 10_000
77+ )
78+ return page
4279
43- assert _port_free (8113 ), "Port 8113 already in use"
80+
81+ def _apply_rate_limit_env (env : dict [str , str ]) -> dict [str , str ]:
82+ env ["LOGIN_IP_LIMIT" ] = "500"
83+ env ["LOGIN_EMAIL_LIMIT" ] = "500"
84+ env ["REGISTER_IP_LIMIT" ] = "500"
85+ env ["FORGOT_PASSWORD_IP_LIMIT" ] = "500"
86+ env ["FORGOT_PASSWORD_EMAIL_LIMIT" ] = "500"
87+ return env
88+
89+
90+ def _start_live_server (env : dict [str , str ], port : int ) -> subprocess .Popen :
91+ assert _port_free (port ), f"Port { port } already in use"
92+ with _temporary_env (env ):
93+ ensure_database_exists (get_connection_url ())
94+ set_up_db (drop = True )
4495
4596 proc = subprocess .Popen (
46- ["uv" , "run" , "uvicorn" , "main:app" , "--host" , "127.0.0.1" , "--port" , "8113" ],
47- env = browser_env ,
97+ [
98+ "uv" ,
99+ "run" ,
100+ "uvicorn" ,
101+ "main:app" ,
102+ "--host" ,
103+ "127.0.0.1" ,
104+ "--port" ,
105+ str (port ),
106+ ],
107+ env = env ,
48108 stdout = subprocess .PIPE ,
49109 stderr = subprocess .PIPE ,
50110 )
51111
52- # Wait for server to be ready
53112 for _ in range (30 ):
54- if not _port_free (8113 ):
113+ if not _port_free (port ):
55114 break
56115 time .sleep (0.5 )
57116 else :
58117 proc .terminate ()
59- raise RuntimeError ("Server did not start within 15 seconds" )
118+ raise RuntimeError (f"Server did not start on port { port } within 15 seconds" )
119+ return proc
60120
61- yield "http://127.0.0.1:8113"
62121
63- proc .terminate ()
64- proc .wait (timeout = 5 )
65- tear_down_db ()
122+ @pytest .fixture (scope = "session" )
123+ def browser_env ():
124+ """Build an environment dict for the live server subprocess."""
125+ env = _apply_rate_limit_env (browser_db_env ())
126+ env ["BASE_URL" ] = "http://127.0.0.1:8113"
127+ env ["CSRF_ENABLED" ] = "0"
128+ return env
129+
130+
131+ @pytest .fixture (scope = "session" )
132+ def browser_csrf_env ():
133+ """Live-server env with CSRF protection enabled (separate port/DB)."""
134+ env = _apply_rate_limit_env (browser_db_env ())
135+ env ["DB_NAME" ] = "webapp-browser-csrf-test-db"
136+ env ["BASE_URL" ] = "http://127.0.0.1:8114"
137+ env ["CSRF_ENABLED" ] = "1"
138+ return env
139+
140+
141+ @pytest .fixture (scope = "session" )
142+ def live_server (browser_env ):
143+ """Start a uvicorn server for Playwright tests and return the base URL."""
144+ proc = _start_live_server (browser_env , 8113 )
145+ try :
146+ yield "http://127.0.0.1:8113"
147+ finally :
148+ proc .terminate ()
149+ proc .wait (timeout = 5 )
150+ with _temporary_env (browser_env ):
151+ tear_down_db ()
152+
153+
154+ @pytest .fixture (scope = "session" )
155+ def live_server_csrf (browser_csrf_env ):
156+ """Live server with CSRF_ENABLED=1 for CSRF browser tests."""
157+ proc = _start_live_server (browser_csrf_env , 8114 )
158+ try :
159+ yield "http://127.0.0.1:8114"
160+ finally :
161+ proc .terminate ()
162+ proc .wait (timeout = 5 )
163+ with _temporary_env (browser_csrf_env ):
164+ tear_down_db ()
0 commit comments