diff --git a/.github/workflows/generate-client.yml b/.github/workflows/generate-client.yml index 1cf417ab89..b1787b0489 100644 --- a/.github/workflows/generate-client.yml +++ b/.github/workflows/generate-client.yml @@ -39,6 +39,10 @@ jobs: - run: uv run bash scripts/generate-client.sh env: VIRTUAL_ENV: backend/.venv + ENVIRONMENT: production + SECRET_KEY: just-for-generating-client + POSTGRES_PASSWORD: just-for-generating-client + FIRST_SUPERUSER_PASSWORD: just-for-generating-client - name: Add changes to git run: | git config --local user.email "github-actions@github.com" diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8c741221f7..b78f32c623 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -59,6 +59,18 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.4.15" + enable-cache: true + - run: uv sync + working-directory: backend + - run: npm ci + working-directory: frontend + - run: uv run bash scripts/generate-client.sh + env: + VIRTUAL_ENV: backend/.venv - run: docker compose build - run: docker compose down -v --remove-orphans - name: Run Playwright tests diff --git a/backend/app/api/main.py b/backend/app/api/main.py index 09e0663fc3..45e930c4a1 100644 --- a/backend/app/api/main.py +++ b/backend/app/api/main.py @@ -1,9 +1,14 @@ from fastapi import APIRouter -from app.api.routes import items, login, users, utils +from app.api.routes import items, login, private, users, utils +from app.core.config import settings api_router = APIRouter() api_router.include_router(login.router, tags=["login"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) api_router.include_router(items.router, prefix="/items", tags=["items"]) + + +if settings.ENVIRONMENT == "local": + api_router.include_router(private.router) diff --git a/backend/app/api/routes/private.py b/backend/app/api/routes/private.py new file mode 100644 index 0000000000..9f33ef1900 --- /dev/null +++ b/backend/app/api/routes/private.py @@ -0,0 +1,38 @@ +from typing import Any + +from fastapi import APIRouter +from pydantic import BaseModel + +from app.api.deps import SessionDep +from app.core.security import get_password_hash +from app.models import ( + User, + UserPublic, +) + +router = APIRouter(tags=["private"], prefix="/private") + + +class PrivateUserCreate(BaseModel): + email: str + password: str + full_name: str + is_verified: bool = False + + +@router.post("/users/", response_model=UserPublic) +def create_user(user_in: PrivateUserCreate, session: SessionDep) -> Any: + """ + Create a new user. + """ + + user = User( + email=user_in.email, + full_name=user_in.full_name, + hashed_password=get_password_hash(user_in.password), + ) + + session.add(user) + session.commit() + + return user diff --git a/backend/app/tests/api/routes/test_private.py b/backend/app/tests/api/routes/test_private.py new file mode 100644 index 0000000000..1e1f985021 --- /dev/null +++ b/backend/app/tests/api/routes/test_private.py @@ -0,0 +1,26 @@ +from fastapi.testclient import TestClient +from sqlmodel import Session, select + +from app.core.config import settings +from app.models import User + + +def test_create_user(client: TestClient, db: Session) -> None: + r = client.post( + f"{settings.API_V1_STR}/private/users/", + json={ + "email": "pollo@listo.com", + "password": "password123", + "full_name": "Pollo Listo", + }, + ) + + assert r.status_code == 200 + + data = r.json() + + user = db.exec(select(User).where(User.id == data["id"])).first() + + assert user + assert user.email == "pollo@listo.com" + assert user.full_name == "Pollo Listo" diff --git a/frontend/package.json b/frontend/package.json index 84f9841485..546028c9a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc -p tsconfig.build.json && vite build", "lint": "biome check --apply-unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./", "preview": "vite preview", "generate-client": "openapi-ts" diff --git a/frontend/tests/user-settings.spec.ts b/frontend/tests/user-settings.spec.ts index a3a8a27490..8175d155c4 100644 --- a/frontend/tests/user-settings.spec.ts +++ b/frontend/tests/user-settings.spec.ts @@ -1,7 +1,8 @@ import { expect, test } from "@playwright/test" import { firstSuperuser, firstSuperuserPassword } from "./config.ts" import { randomEmail, randomPassword } from "./utils/random" -import { logInUser, logOutUser, signUpNewUser } from "./utils/user" +import { logInUser, logOutUser } from "./utils/user" +import { createUser } from "./utils/privateApi.ts" const tabs = ["My profile", "Password", "Appearance"] @@ -26,13 +27,11 @@ test.describe("Edit user full name and email successfully", () => { test.use({ storageState: { cookies: [], origins: [] } }) test("Edit user name with a valid name", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const updatedName = "Test User 2" const password = randomPassword() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -50,13 +49,11 @@ test.describe("Edit user full name and email successfully", () => { }) test("Edit user email with a valid email", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const updatedEmail = randomEmail() const password = randomPassword() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -77,13 +74,11 @@ test.describe("Edit user with invalid data", () => { test.use({ storageState: { cookies: [], origins: [] } }) test("Edit user email with an invalid email", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const invalidEmail = "" - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -97,13 +92,11 @@ test.describe("Edit user with invalid data", () => { }) test("Cancel edit action restores original name", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const updatedName = "Test User" - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + const user = await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -114,18 +107,18 @@ test.describe("Edit user with invalid data", () => { await page.getByLabel("Full name").fill(updatedName) await page.getByRole("button", { name: "Cancel" }).first().click() await expect( - page.getByLabel("My profile").getByText(fullName, { exact: true }), + page + .getByLabel("My profile") + .getByText(user.full_name as string, { exact: true }), ).toBeVisible() }) test("Cancel edit action restores original email", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const updatedEmail = randomEmail() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -147,13 +140,11 @@ test.describe("Change password successfully", () => { test.use({ storageState: { cookies: [], origins: [] } }) test("Update password successfully", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const NewPassword = randomPassword() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -177,13 +168,11 @@ test.describe("Change password with invalid data", () => { test.use({ storageState: { cookies: [], origins: [] } }) test("Update password with weak passwords", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const weakPassword = "weak" - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -201,14 +190,12 @@ test.describe("Change password with invalid data", () => { test("New password and confirmation password do not match", async ({ page, }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() const newPassword = randomPassword() const confirmPassword = randomPassword() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) @@ -223,12 +210,10 @@ test.describe("Change password with invalid data", () => { }) test("Current password and new password are the same", async ({ page }) => { - const fullName = "Test User" const email = randomEmail() const password = randomPassword() - // Sign up a new user - await signUpNewUser(page, fullName, email, password) + await createUser({ email, password }) // Log in the user await logInUser(page, email, password) diff --git a/frontend/tests/utils/privateApi.ts b/frontend/tests/utils/privateApi.ts new file mode 100644 index 0000000000..b6fa0afe67 --- /dev/null +++ b/frontend/tests/utils/privateApi.ts @@ -0,0 +1,22 @@ +// Note: the `PrivateService` is only available when generating the client +// for local environments +import { OpenAPI, PrivateService } from "../../src/client" + +OpenAPI.BASE = `${process.env.VITE_API_URL}` + +export const createUser = async ({ + email, + password, +}: { + email: string + password: string +}) => { + return await PrivateService.createUser({ + requestBody: { + email, + password, + is_verified: true, + full_name: "Test User", + }, + }) +} diff --git a/frontend/tsconfig.build.json b/frontend/tsconfig.build.json new file mode 100644 index 0000000000..13bd2efc21 --- /dev/null +++ b/frontend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["tests/**/*.ts"] +}