forked from cartsnitch/cartsnitch
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 121dc5724e | |||
| adfa34f2c2 | |||
| ade03fdd1c |
@@ -10,7 +10,6 @@ test.describe('J1: Registration and Login', () => {
|
|||||||
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
|
await page.fill('[placeholder="Password (min. 8 characters)"]', 'TestPass123!');
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
|
|
||||||
// With VITE_MOCK_AUTH=true the app navigates to "/" on success
|
|
||||||
await expect(page).toHaveURL('http://localhost:5173/');
|
await expect(page).toHaveURL('http://localhost:5173/');
|
||||||
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
|
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'VITE_MOCK_AUTH=true npm run dev',
|
command: 'npm run dev',
|
||||||
url: 'http://localhost:5173',
|
url: 'http://localhost:5173',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
"""Service-specific configuration for ReceiptWitness."""
|
"""Service-specific configuration for ReceiptWitness."""
|
||||||
|
|
||||||
|
from pydantic import model_validator
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
_PLACEHOLDER_VALUES = {"change-me-in-production"}
|
||||||
|
|
||||||
|
|
||||||
class ReceiptWitnessSettings(BaseSettings):
|
class ReceiptWitnessSettings(BaseSettings):
|
||||||
model_config = {"env_prefix": "RW_"}
|
model_config = {"env_prefix": "RW_"}
|
||||||
|
|
||||||
@@ -30,5 +34,34 @@ class ReceiptWitnessSettings(BaseSettings):
|
|||||||
# Mailgun inbound email webhook
|
# Mailgun inbound email webhook
|
||||||
mailgun_webhook_signing_key: str = ""
|
mailgun_webhook_signing_key: str = ""
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_required_vars(self):
|
||||||
|
errors = []
|
||||||
|
if not self.session_encryption_key or self.session_encryption_key in _PLACEHOLDER_VALUES:
|
||||||
|
errors.append(
|
||||||
|
"RW_SESSION_ENCRYPTION_KEY must be set to a secure value. "
|
||||||
|
'Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"'
|
||||||
|
)
|
||||||
|
if self.notifications_enabled and not self.resend_api_key:
|
||||||
|
errors.append(
|
||||||
|
"RW_RESEND_API_KEY must be set when RW_NOTIFICATIONS_ENABLED=true. "
|
||||||
|
"Get an API key from https://resend.com/api-keys"
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
raise ValueError(
|
||||||
|
"ReceiptWitness startup failed — missing required config:\n"
|
||||||
|
+ "\n".join(f" - {e}" for e in errors)
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
settings = ReceiptWitnessSettings()
|
|
||||||
|
class _LazySettings:
|
||||||
|
_instance: ReceiptWitnessSettings | None = None
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
if _LazySettings._instance is None:
|
||||||
|
_LazySettings._instance = ReceiptWitnessSettings()
|
||||||
|
return getattr(_LazySettings._instance, name)
|
||||||
|
|
||||||
|
|
||||||
|
settings = _LazySettings()
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
"""Shared test fixtures."""
|
"""Shared test fixtures."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
||||||
|
|
||||||
|
os.environ.setdefault("RW_SESSION_ENCRYPTION_KEY", "test-secret-key-for-unit-tests-only-32bytes!")
|
||||||
|
os.environ.setdefault("RW_MAILGUN_WEBHOOK_SIGNING_KEY", "test-mailgun-signing-key")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def meijer_receipt_data() -> dict:
|
def meijer_receipt_data() -> dict:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import pytest
|
||||||
|
from receiptwitness.config import ReceiptWitnessSettings
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_config():
|
||||||
|
s = ReceiptWitnessSettings(
|
||||||
|
session_encryption_key="7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8="
|
||||||
|
)
|
||||||
|
assert s.session_encryption_key
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_session_encryption_key_raises():
|
||||||
|
with pytest.raises(ValueError, match="RW_SESSION_ENCRYPTION_KEY"):
|
||||||
|
ReceiptWitnessSettings(session_encryption_key="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_placeholder_session_encryption_key_raises():
|
||||||
|
with pytest.raises(ValueError, match="RW_SESSION_ENCRYPTION_KEY"):
|
||||||
|
ReceiptWitnessSettings(session_encryption_key="change-me-in-production")
|
||||||
|
|
||||||
|
|
||||||
|
def test_notifications_enabled_without_resend_key_raises():
|
||||||
|
with pytest.raises(ValueError, match="RW_RESEND_API_KEY"):
|
||||||
|
ReceiptWitnessSettings(
|
||||||
|
session_encryption_key="7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=",
|
||||||
|
notifications_enabled=True,
|
||||||
|
resend_api_key="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_notifications_disabled_without_resend_key_ok():
|
||||||
|
s = ReceiptWitnessSettings(
|
||||||
|
session_encryption_key="7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=",
|
||||||
|
notifications_enabled=False,
|
||||||
|
resend_api_key="",
|
||||||
|
)
|
||||||
|
assert s.notifications_enabled is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_notifications_enabled_with_resend_key_ok():
|
||||||
|
s = ReceiptWitnessSettings(
|
||||||
|
session_encryption_key="7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=",
|
||||||
|
notifications_enabled=True,
|
||||||
|
resend_api_key="re_test_1234567890",
|
||||||
|
)
|
||||||
|
assert s.resend_api_key == "re_test_1234567890"
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { Navigate, Outlet } from 'react-router-dom'
|
import { Navigate, Outlet } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
import { useAuthStore } from '../stores/auth.ts'
|
|
||||||
|
|
||||||
export function ProtectedRoute() {
|
export function ProtectedRoute() {
|
||||||
const isMockAuth = import.meta.env.VITE_MOCK_AUTH === 'true'
|
|
||||||
const { data: session, isPending } = authClient.useSession()
|
const { data: session, isPending } = authClient.useSession()
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
|
|
||||||
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isMockAuth) {
|
|
||||||
setAuthenticated(!!session)
|
|
||||||
}
|
|
||||||
}, [session, setAuthenticated, isMockAuth])
|
|
||||||
|
|
||||||
// In mock auth mode, rely on Zustand store (set by Login/Register pages)
|
|
||||||
if (isMockAuth) {
|
|
||||||
if (!isAuthenticated) return <Navigate to="/login" replace />
|
|
||||||
return <Outlet />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+1
-8
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
import { useAuthStore } from '../stores/auth.ts'
|
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -9,7 +8,6 @@ export function Login() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -40,12 +38,7 @@ export function Login() {
|
|||||||
setError('Sign in failed. Please try again.')
|
setError('Sign in failed. Please try again.')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
setError('Invalid email or password. Please try again.')
|
||||||
setAuthenticated(true)
|
|
||||||
navigate('/')
|
|
||||||
} else {
|
|
||||||
setError('Invalid email or password. Please try again.')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { authClient } from '../lib/auth-client.ts'
|
import { authClient } from '../lib/auth-client.ts'
|
||||||
import { useAuthStore } from '../stores/auth.ts'
|
|
||||||
|
|
||||||
export function Register() {
|
export function Register() {
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
@@ -10,7 +9,6 @@ export function Register() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const setAuthenticated = useAuthStore((s) => s.setAuthenticated)
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -48,12 +46,7 @@ export function Register() {
|
|||||||
setError('Account created! Please sign in.')
|
setError('Account created! Please sign in.')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (import.meta.env.VITE_MOCK_AUTH === 'true') {
|
setError('Registration failed. Please try again.')
|
||||||
setAuthenticated(true)
|
|
||||||
navigate('/')
|
|
||||||
} else {
|
|
||||||
setError('Registration failed. Please try again.')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user