Refactor code for improved readability and consistency across templates and frontend logic
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -70,7 +70,8 @@ def login():
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
email = request.form["email"]
|
email = request.form["email"]
|
||||||
password = request.form["password"]
|
password = request.form["password"]
|
||||||
resp = _api("POST", "/auth/login", json={"email": email, "password": password})
|
resp = _api("POST", "/auth/login",
|
||||||
|
json={"email": email, "password": password})
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
session["access_token"] = data["access_token"]
|
session["access_token"] = data["access_token"]
|
||||||
@@ -90,7 +91,8 @@ def register():
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
email = request.form["email"]
|
email = request.form["email"]
|
||||||
password = request.form["password"]
|
password = request.form["password"]
|
||||||
resp = _api("POST", "/auth/register", json={"email": email, "password": password})
|
resp = _api("POST", "/auth/register",
|
||||||
|
json={"email": email, "password": password})
|
||||||
if resp.status_code == 201:
|
if resp.status_code == 201:
|
||||||
flash("Account created. Please log in.", "success")
|
flash("Account created. Please log in.", "success")
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
@@ -207,7 +209,8 @@ def admin():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def admin_set_role(user_id: str):
|
def admin_set_role(user_id: str):
|
||||||
role = request.form.get("role", "user")
|
role = request.form.get("role", "user")
|
||||||
_api("PUT", f"/users/{user_id}/role", token=session["access_token"], json={"role": role})
|
_api("PUT", f"/users/{user_id}/role",
|
||||||
|
token=session["access_token"], json={"role": role})
|
||||||
flash(f"Role updated to '{role}'.", "success")
|
flash(f"Role updated to '{role}'.", "success")
|
||||||
return redirect(url_for("admin"))
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
@@ -238,7 +241,8 @@ def profile():
|
|||||||
resp = _api("PUT", "/users/me", token=token, json=payload)
|
resp = _api("PUT", "/users/me", token=token, json=payload)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
updated = resp.json()
|
updated = resp.json()
|
||||||
session["user_email"] = updated.get("email", session.get("user_email", ""))
|
session["user_email"] = updated.get(
|
||||||
|
"email", session.get("user_email", ""))
|
||||||
flash("Profile updated.", "success")
|
flash("Profile updated.", "success")
|
||||||
else:
|
else:
|
||||||
flash(resp.json().get("detail", "Update failed."), "error")
|
flash(resp.json().get("detail", "Update failed."), "error")
|
||||||
@@ -246,4 +250,3 @@ def profile():
|
|||||||
resp = _api("GET", "/users/me", token=token)
|
resp = _api("GET", "/users/me", token=token)
|
||||||
user = resp.json() if resp.status_code == 200 else {}
|
user = resp.json() if resp.status_code == 200 else {}
|
||||||
return render_template("profile.html", user=user)
|
return render_template("profile.html", user=user)
|
||||||
|
|
||||||
|
|||||||
+20
-16
@@ -1,36 +1,40 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// ── Loading overlay ────────────────────────────────────
|
// ── Loading overlay ────────────────────────────────────
|
||||||
const overlay = document.getElementById('loading-overlay');
|
const overlay = document.getElementById("loading-overlay");
|
||||||
|
|
||||||
document.querySelectorAll('form').forEach((form) => {
|
document.querySelectorAll("form").forEach((form) => {
|
||||||
form.addEventListener('submit', () => {
|
form.addEventListener("submit", () => {
|
||||||
if (overlay) overlay.classList.add('active');
|
if (overlay) overlay.classList.add("active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Hamburger menu ─────────────────────────────────────
|
// ── Hamburger menu ─────────────────────────────────────
|
||||||
const hamburger = document.querySelector('.hamburger');
|
const hamburger = document.querySelector(".hamburger");
|
||||||
const navLinks = document.querySelector('.nav-links');
|
const navLinks = document.querySelector(".nav-links");
|
||||||
|
|
||||||
if (hamburger && navLinks) {
|
if (hamburger && navLinks) {
|
||||||
hamburger.addEventListener('click', () => {
|
hamburger.addEventListener("click", () => {
|
||||||
navLinks.classList.toggle('open');
|
navLinks.classList.toggle("open");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generate dropdown tabs ─────────────────────────────
|
// ── Generate dropdown tabs ─────────────────────────────
|
||||||
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
document.querySelectorAll(".tab-btn").forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener("click", () => {
|
||||||
const target = btn.dataset.tab;
|
const target = btn.dataset.tab;
|
||||||
const container = btn.closest('.tabs-container');
|
const container = btn.closest(".tabs-container");
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
|
container
|
||||||
container.querySelectorAll('.tab-panel').forEach((p) => p.classList.remove('active'));
|
.querySelectorAll(".tab-btn")
|
||||||
|
.forEach((b) => b.classList.remove("active"));
|
||||||
|
container
|
||||||
|
.querySelectorAll(".tab-panel")
|
||||||
|
.forEach((p) => p.classList.remove("active"));
|
||||||
|
|
||||||
btn.classList.add('active');
|
btn.classList.add("active");
|
||||||
const panel = container.querySelector(`#tab-${target}`);
|
const panel = container.querySelector(`#tab-${target}`);
|
||||||
if (panel) panel.classList.add('active');
|
if (panel) panel.classList.add("active");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}Admin — AI Allucanget{% endblock %}
|
||||||
{% block title %}Admin — AI Allucanget{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>Admin Dashboard</h1>
|
<h1>Admin Dashboard</h1>
|
||||||
@@ -41,25 +40,38 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<!-- Role toggle -->
|
<!-- Role toggle -->
|
||||||
<form method="post" action="{{ url_for('admin_set_role', user_id=u.id) }}">
|
<form
|
||||||
<input type="hidden" name="role"
|
method="post"
|
||||||
value="{{ 'user' if u.role == 'admin' else 'admin' }}">
|
action="{{ url_for('admin_set_role', user_id=u.id) }}"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="role"
|
||||||
|
value="{{ 'user' if u.role == 'admin' else 'admin' }}"
|
||||||
|
/>
|
||||||
<button type="submit" class="btn btn-sm">
|
<button type="submit" class="btn btn-sm">
|
||||||
Make {{ 'user' if u.role == 'admin' else 'admin' }}
|
Make {{ 'user' if u.role == 'admin' else 'admin' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
{% if u.id != session.get('user_id') %}
|
{% if u.id != session.get('user_id') %}
|
||||||
<form method="post" action="{{ url_for('admin_delete_user', user_id=u.id) }}"
|
<form
|
||||||
onsubmit="return confirm('Delete {{ u.email }}?')">
|
method="post"
|
||||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
action="{{ url_for('admin_delete_user', user_id=u.id) }}"
|
||||||
|
onsubmit="return confirm('Delete {{ u.email }}?')"
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="3" class="text-muted">No users found.</td></tr>
|
<tr>
|
||||||
|
<td colspan="3" class="text-muted">No users found.</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}Text Generation — AI Allucanget{%
|
||||||
{% block title %}Text Generation — AI Allucanget{% endblock %}
|
endblock %} {% block content %}
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>Text Generation</h1>
|
<h1>Text Generation</h1>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label for="model">Model</label>
|
<label for="model">Model</label>
|
||||||
<input id="model" name="model" type="text" required
|
<input
|
||||||
|
id="model"
|
||||||
|
name="model"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
placeholder="e.g. openai/gpt-4o"
|
placeholder="e.g. openai/gpt-4o"
|
||||||
value="{{ request.form.get('model', '') }}">
|
value="{{ request.form.get('model', '') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="prompt">Prompt</label>
|
<label for="prompt">Prompt</label>
|
||||||
<textarea id="prompt" name="prompt" rows="5" required
|
<textarea
|
||||||
placeholder="Describe what you want…">{{ request.form.get('prompt', '') }}</textarea>
|
id="prompt"
|
||||||
|
name="prompt"
|
||||||
|
rows="5"
|
||||||
|
required
|
||||||
|
placeholder="Describe what you want…"
|
||||||
|
>
|
||||||
|
{{ request.form.get('prompt', '') }}</textarea
|
||||||
|
>
|
||||||
|
|
||||||
<button type="submit">Generate text</button>
|
<button type="submit">Generate text</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error mt-2">{{ error }}</div>
|
<div class="alert alert-error mt-2">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %} {% if result %}
|
||||||
|
|
||||||
{% if result %}
|
|
||||||
<div class="result">
|
<div class="result">
|
||||||
<h2>Result</h2>
|
<h2>Result</h2>
|
||||||
<pre>{{ result.content }}</pre>
|
<pre>{{ result.content }}</pre>
|
||||||
{% if result.usage %}
|
{% if result.usage %}
|
||||||
<p class="text-muted mt-1" style="font-size:0.8rem;">
|
<p class="text-muted mt-1" style="font-size: 0.8rem">
|
||||||
Tokens: {{ result.usage.get('total_tokens', '—') }}
|
Tokens: {{ result.usage.get('total_tokens', '—') }}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,28 +1,43 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}Video Generation — AI Allucanget{%
|
||||||
{% block title %}Video Generation — AI Allucanget{% endblock %}
|
endblock %} {% block content %}
|
||||||
{% block content %}
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h1>Video Generation</h1>
|
<h1>Video Generation</h1>
|
||||||
|
|
||||||
<div class="tabs-container">
|
<div class="tabs-container">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="text-to-video" type="button">Text to video</button>
|
<button class="tab-btn active" data-tab="text-to-video" type="button">
|
||||||
<button class="tab-btn" data-tab="image-to-video" type="button">Image to video</button>
|
Text to video
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn" data-tab="image-to-video" type="button">
|
||||||
|
Image to video
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Text-to-video -->
|
<!-- Text-to-video -->
|
||||||
<div class="tab-panel active" id="tab-text-to-video">
|
<div class="tab-panel active" id="tab-text-to-video">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="mode" value="text">
|
<input type="hidden" name="mode" value="text" />
|
||||||
|
|
||||||
<label for="model-t">Model</label>
|
<label for="model-t">Model</label>
|
||||||
<input id="model-t" name="model" type="text" required
|
<input
|
||||||
|
id="model-t"
|
||||||
|
name="model"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
placeholder="e.g. openai/sora-2-pro"
|
placeholder="e.g. openai/sora-2-pro"
|
||||||
value="{{ request.form.get('model', '') if request.form.get('mode','text')=='text' else '' }}">
|
value="{{ request.form.get('model', '') if request.form.get('mode','text')=='text' else '' }}"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="prompt-t">Prompt</label>
|
<label for="prompt-t">Prompt</label>
|
||||||
<textarea id="prompt-t" name="prompt" rows="4" required
|
<textarea
|
||||||
placeholder="Describe the video you want…">{{ request.form.get('prompt', '') if request.form.get('mode','text')=='text' else '' }}</textarea>
|
id="prompt-t"
|
||||||
|
name="prompt"
|
||||||
|
rows="4"
|
||||||
|
required
|
||||||
|
placeholder="Describe the video you want…"
|
||||||
|
>
|
||||||
|
{{ request.form.get('prompt', '') if request.form.get('mode','text')=='text' else '' }}</textarea
|
||||||
|
>
|
||||||
|
|
||||||
<label for="aspect-t">Aspect ratio</label>
|
<label for="aspect-t">Aspect ratio</label>
|
||||||
<select id="aspect-t" name="aspect_ratio">
|
<select id="aspect-t" name="aspect_ratio">
|
||||||
@@ -38,21 +53,38 @@
|
|||||||
<!-- Image-to-video -->
|
<!-- Image-to-video -->
|
||||||
<div class="tab-panel" id="tab-image-to-video">
|
<div class="tab-panel" id="tab-image-to-video">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="mode" value="image">
|
<input type="hidden" name="mode" value="image" />
|
||||||
|
|
||||||
<label for="model-i">Model</label>
|
<label for="model-i">Model</label>
|
||||||
<input id="model-i" name="model" type="text" required
|
<input
|
||||||
|
id="model-i"
|
||||||
|
name="model"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
placeholder="e.g. openai/sora-2-pro"
|
placeholder="e.g. openai/sora-2-pro"
|
||||||
value="{{ request.form.get('model', '') if request.form.get('mode')=='image' else '' }}">
|
value="{{ request.form.get('model', '') if request.form.get('mode')=='image' else '' }}"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="image_url">Source image URL</label>
|
<label for="image_url">Source image URL</label>
|
||||||
<input id="image_url" name="image_url" type="url" required
|
<input
|
||||||
|
id="image_url"
|
||||||
|
name="image_url"
|
||||||
|
type="url"
|
||||||
|
required
|
||||||
placeholder="https://example.com/photo.jpg"
|
placeholder="https://example.com/photo.jpg"
|
||||||
value="{{ request.form.get('image_url', '') }}">
|
value="{{ request.form.get('image_url', '') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
<label for="prompt-i">Motion prompt</label>
|
<label for="prompt-i">Motion prompt</label>
|
||||||
<textarea id="prompt-i" name="prompt" rows="3" required
|
<textarea
|
||||||
placeholder="Describe the motion or transformation…">{{ request.form.get('prompt', '') if request.form.get('mode')=='image' else '' }}</textarea>
|
id="prompt-i"
|
||||||
|
name="prompt"
|
||||||
|
rows="3"
|
||||||
|
required
|
||||||
|
placeholder="Describe the motion or transformation…"
|
||||||
|
>
|
||||||
|
{{ request.form.get('prompt', '') if request.form.get('mode')=='image' else '' }}</textarea
|
||||||
|
>
|
||||||
|
|
||||||
<label for="aspect-i">Aspect ratio</label>
|
<label for="aspect-i">Aspect ratio</label>
|
||||||
<select id="aspect-i" name="aspect_ratio">
|
<select id="aspect-i" name="aspect_ratio">
|
||||||
@@ -68,16 +100,18 @@
|
|||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-error mt-2">{{ error }}</div>
|
<div class="alert alert-error mt-2">{{ error }}</div>
|
||||||
{% endif %}
|
{% endif %} {% if result %}
|
||||||
|
|
||||||
{% if result %}
|
|
||||||
<div class="result">
|
<div class="result">
|
||||||
<h2>Video job</h2>
|
<h2>Video job</h2>
|
||||||
<p>Status: <strong>{{ result.status }}</strong></p>
|
<p>Status: <strong>{{ result.status }}</strong></p>
|
||||||
{% if result.get('video_url') %}
|
{% if result.get('video_url') %}
|
||||||
<video src="{{ result.video_url }}" controls class="generated-video"></video>
|
<video
|
||||||
|
src="{{ result.video_url }}"
|
||||||
|
controls
|
||||||
|
class="generated-video"
|
||||||
|
></video>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted mt-1" style="font-size:0.875rem;">
|
<p class="text-muted mt-1" style="font-size: 0.875rem">
|
||||||
Video is being processed. Check back later.
|
Video is being processed. Check back later.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+203
-43
@@ -17,13 +17,21 @@ def client():
|
|||||||
yield c
|
yield c
|
||||||
|
|
||||||
|
|
||||||
def _mock_response(status_code: int, json_data: dict) -> MagicMock:
|
def _mock_response(status_code: int, json_data) -> MagicMock:
|
||||||
m = MagicMock()
|
m = MagicMock()
|
||||||
m.status_code = status_code
|
m.status_code = status_code
|
||||||
m.json.return_value = json_data
|
m.json.return_value = json_data
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _set_auth(client, role: str = "user"):
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
sess["access_token"] = "tok"
|
||||||
|
sess["refresh_token"] = "ref"
|
||||||
|
sess["user_role"] = role
|
||||||
|
sess["user_email"] = "u@example.com"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Index redirect
|
# Index redirect
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -35,9 +43,7 @@ def test_index_redirects_to_login(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_index_redirects_to_dashboard_when_logged_in(client):
|
def test_index_redirects_to_dashboard_when_logged_in(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
|
||||||
sess["refresh_token"] = "ref"
|
|
||||||
resp = client.get("/")
|
resp = client.get("/")
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "/dashboard" in resp.headers["Location"]
|
assert "/dashboard" in resp.headers["Location"]
|
||||||
@@ -54,17 +60,34 @@ def test_login_page_renders(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_login_success(client):
|
def test_login_success(client):
|
||||||
mock = _mock_response(200, {"access_token": "acc", "refresh_token": "ref"})
|
login_mock = _mock_response(
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
200, {"access_token": "acc", "refresh_token": "ref"})
|
||||||
resp = client.post("/login", data={"email": "u@example.com", "password": "secret"})
|
me_mock = _mock_response(
|
||||||
|
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||||
|
with patch("frontend.app.main.httpx.request", side_effect=[login_mock, me_mock]):
|
||||||
|
resp = client.post(
|
||||||
|
"/login", data={"email": "u@example.com", "password": "secret"})
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "/dashboard" in resp.headers["Location"]
|
assert "/dashboard" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_stores_role_in_session(client):
|
||||||
|
login_mock = _mock_response(
|
||||||
|
200, {"access_token": "acc", "refresh_token": "ref"})
|
||||||
|
me_mock = _mock_response(
|
||||||
|
200, {"id": "1", "email": "admin@example.com", "role": "admin"})
|
||||||
|
with patch("frontend.app.main.httpx.request", side_effect=[login_mock, me_mock]):
|
||||||
|
client.post(
|
||||||
|
"/login", data={"email": "admin@example.com", "password": "secret"})
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
assert sess["user_role"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
def test_login_failure_shows_error(client):
|
def test_login_failure_shows_error(client):
|
||||||
mock = _mock_response(401, {"detail": "Invalid credentials."})
|
mock = _mock_response(401, {"detail": "Invalid credentials."})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/login", data={"email": "u@example.com", "password": "wrong"})
|
resp = client.post(
|
||||||
|
"/login", data={"email": "u@example.com", "password": "wrong"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert b"Invalid email or password" in resp.data
|
assert b"Invalid email or password" in resp.data
|
||||||
|
|
||||||
@@ -80,9 +103,11 @@ def test_register_page_renders(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_register_success_redirects_to_login(client):
|
def test_register_success_redirects_to_login(client):
|
||||||
mock = _mock_response(201, {"id": "abc", "email": "u@example.com", "role": "user"})
|
mock = _mock_response(
|
||||||
|
201, {"id": "abc", "email": "u@example.com", "role": "user"})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/register", data={"email": "u@example.com", "password": "secret123"})
|
resp = client.post(
|
||||||
|
"/register", data={"email": "u@example.com", "password": "secret123"})
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
assert "/login" in resp.headers["Location"]
|
assert "/login" in resp.headers["Location"]
|
||||||
|
|
||||||
@@ -90,7 +115,8 @@ def test_register_success_redirects_to_login(client):
|
|||||||
def test_register_duplicate_shows_error(client):
|
def test_register_duplicate_shows_error(client):
|
||||||
mock = _mock_response(409, {"detail": "Email already registered."})
|
mock = _mock_response(409, {"detail": "Email already registered."})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/register", data={"email": "dup@example.com", "password": "secret123"})
|
resp = client.post(
|
||||||
|
"/register", data={"email": "dup@example.com", "password": "secret123"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert b"Email already registered" in resp.data
|
assert b"Email already registered" in resp.data
|
||||||
|
|
||||||
@@ -100,9 +126,7 @@ def test_register_duplicate_shows_error(client):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_logout_clears_session_and_redirects(client):
|
def test_logout_clears_session_and_redirects(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
|
||||||
sess["refresh_token"] = "ref"
|
|
||||||
mock = _mock_response(204, {})
|
mock = _mock_response(204, {})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.get("/logout")
|
resp = client.get("/logout")
|
||||||
@@ -123,9 +147,9 @@ def test_dashboard_requires_login(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_dashboard_renders_user_info(client):
|
def test_dashboard_renders_user_info(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
mock = _mock_response(
|
||||||
mock = _mock_response(200, {"id": "1", "email": "u@example.com", "role": "user"})
|
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.get("/dashboard")
|
resp = client.get("/dashboard")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@@ -133,57 +157,193 @@ def test_dashboard_renders_user_info(client):
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Generate
|
# Generate — redirect + separate pages
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def test_generate_page_requires_login(client):
|
def test_generate_redirects_to_text(client):
|
||||||
|
_set_auth(client)
|
||||||
resp = client.get("/generate")
|
resp = client.get("/generate")
|
||||||
assert resp.status_code == 302
|
assert resp.status_code == 302
|
||||||
|
assert "/generate/text" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_page_renders(client):
|
||||||
|
_set_auth(client)
|
||||||
|
resp = client.get("/generate/text")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"Text Generation" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_requires_login(client):
|
||||||
|
resp = client.get("/generate/text")
|
||||||
|
assert resp.status_code == 302
|
||||||
assert "/login" in resp.headers["Location"]
|
assert "/login" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_page_renders(client):
|
|
||||||
with client.session_transaction() as sess:
|
|
||||||
sess["access_token"] = "tok"
|
|
||||||
resp = client.get("/generate")
|
|
||||||
assert resp.status_code == 200
|
|
||||||
assert b"Generate" in resp.data
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_success(client):
|
def test_generate_text_success(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
mock = _mock_response(
|
||||||
mock = _mock_response(200, {"id": "g1", "model": "openai/gpt-4o", "content": "Hello world", "usage": None})
|
200, {"id": "g1", "model": "openai/gpt-4o", "content": "Hello world", "usage": None})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/generate", data={
|
resp = client.post(
|
||||||
"type": "text", "model": "openai/gpt-4o", "prompt": "Say hello"
|
"/generate/text", data={"model": "openai/gpt-4o", "prompt": "Say hello"})
|
||||||
})
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert b"Hello world" in resp.data
|
assert b"Hello world" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_image_page_renders(client):
|
||||||
|
_set_auth(client)
|
||||||
|
resp = client.get("/generate/image")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"Image Generation" in resp.data
|
||||||
|
|
||||||
|
|
||||||
def test_generate_image_success(client):
|
def test_generate_image_success(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
|
||||||
mock = _mock_response(200, {
|
mock = _mock_response(200, {
|
||||||
"id": "g2", "model": "openai/dall-e-3",
|
"id": "g2", "model": "openai/dall-e-3",
|
||||||
"images": [{"url": "https://example.com/img.png", "revised_prompt": None, "b64_json": None}]
|
"images": [{"url": "https://example.com/img.png", "revised_prompt": None, "b64_json": None}]
|
||||||
})
|
})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/generate", data={
|
resp = client.post("/generate/image", data={
|
||||||
"type": "image", "model": "openai/dall-e-3", "prompt": "A cat"
|
"model": "openai/dall-e-3", "prompt": "A cat", "n": "1", "size": "1024x1024"
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert b"example.com/img.png" in resp.data
|
assert b"example.com/img.png" in resp.data
|
||||||
|
|
||||||
|
|
||||||
def test_generate_upstream_error_shows_message(client):
|
def test_generate_video_page_renders(client):
|
||||||
with client.session_transaction() as sess:
|
_set_auth(client)
|
||||||
sess["access_token"] = "tok"
|
resp = client.get("/generate/video")
|
||||||
mock = _mock_response(502, {"detail": "OpenRouter error: timeout"})
|
assert resp.status_code == 200
|
||||||
|
assert b"Video Generation" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_video_text_mode(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(
|
||||||
|
200, {"id": "v1", "model": "openai/sora-2-pro", "status": "queued"})
|
||||||
with patch("frontend.app.main.httpx.request", return_value=mock):
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
resp = client.post("/generate", data={
|
resp = client.post("/generate/video", data={
|
||||||
"type": "text", "model": "openai/gpt-4o", "prompt": "Hi"
|
"mode": "text", "model": "openai/sora-2-pro",
|
||||||
|
"prompt": "A sunset", "aspect_ratio": "16:9"
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
assert b"queued" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_video_image_mode(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(
|
||||||
|
200, {"id": "v2", "model": "openai/sora-2-pro", "status": "processing"})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post("/generate/video", data={
|
||||||
|
"mode": "image", "model": "openai/sora-2-pro",
|
||||||
|
"image_url": "https://example.com/img.png",
|
||||||
|
"prompt": "Pan right", "aspect_ratio": "16:9"
|
||||||
|
})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"processing" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_upstream_error_shows_message(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(502, {"detail": "OpenRouter error: timeout"})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post(
|
||||||
|
"/generate/text", data={"model": "openai/gpt-4o", "prompt": "Hi"})
|
||||||
|
assert resp.status_code == 200
|
||||||
assert b"OpenRouter error" in resp.data
|
assert b"OpenRouter error" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Admin
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_admin_requires_login(client):
|
||||||
|
resp = client.get("/admin")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/login" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_requires_admin_role(client):
|
||||||
|
_set_auth(client, role="user")
|
||||||
|
resp = client.get("/admin")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/dashboard" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_page_renders(client):
|
||||||
|
_set_auth(client, role="admin")
|
||||||
|
stats_mock = _mock_response(
|
||||||
|
200, {"total_users": 5, "active_refresh_tokens": 3, "admin_users": 1})
|
||||||
|
users_mock = _mock_response(200, [
|
||||||
|
{"id": "u1", "email": "a@example.com", "role": "admin"},
|
||||||
|
{"id": "u2", "email": "b@example.com", "role": "user"},
|
||||||
|
])
|
||||||
|
with patch("frontend.app.main.httpx.request", side_effect=[stats_mock, users_mock]):
|
||||||
|
resp = client.get("/admin")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"a@example.com" in resp.data
|
||||||
|
assert b"b@example.com" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_set_role(client):
|
||||||
|
_set_auth(client, role="admin")
|
||||||
|
mock = _mock_response(
|
||||||
|
200, {"id": "u2", "email": "b@example.com", "role": "admin"})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post("/admin/users/u2/role", data={"role": "admin"})
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/admin" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_delete_user(client):
|
||||||
|
_set_auth(client, role="admin")
|
||||||
|
mock = _mock_response(204, {})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post("/admin/users/u2/delete")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/admin" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Profile
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_profile_requires_login(client):
|
||||||
|
resp = client.get("/users/profile")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/login" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_page_renders(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(
|
||||||
|
200, {"id": "1", "email": "u@example.com", "role": "user"})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.get("/users/profile")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert b"Profile" in resp.data
|
||||||
|
assert b"u@example.com" in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_update_email(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(
|
||||||
|
200, {"id": "1", "email": "new@example.com", "role": "user"})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post(
|
||||||
|
"/users/profile", data={"email": "new@example.com", "password": ""})
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "/users/profile" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_profile_update_failure(client):
|
||||||
|
_set_auth(client)
|
||||||
|
mock = _mock_response(422, {"detail": "Invalid email."})
|
||||||
|
with patch("frontend.app.main.httpx.request", return_value=mock):
|
||||||
|
resp = client.post(
|
||||||
|
"/users/profile", data={"email": "bad", "password": ""})
|
||||||
|
# redirects regardless, flash message shown on next GET
|
||||||
|
assert resp.status_code == 302
|
||||||
|
|||||||
Reference in New Issue
Block a user