✨ Add private, local only, API for usage in E2E tests (#1429)
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
4
.github/workflows/generate-client.yml
vendored
4
.github/workflows/generate-client.yml
vendored
@@ -39,6 +39,10 @@ jobs:
|
|||||||
- run: uv run bash scripts/generate-client.sh
|
- run: uv run bash scripts/generate-client.sh
|
||||||
env:
|
env:
|
||||||
VIRTUAL_ENV: backend/.venv
|
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
|
- name: Add changes to git
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "github-actions@github.com"
|
git config --local user.email "github-actions@github.com"
|
||||||
|
12
.github/workflows/playwright.yml
vendored
12
.github/workflows/playwright.yml
vendored
@@ -59,6 +59,18 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||||
with:
|
with:
|
||||||
limit-access-to-actor: true
|
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 build
|
||||||
- run: docker compose down -v --remove-orphans
|
- run: docker compose down -v --remove-orphans
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
from fastapi import APIRouter
|
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 = APIRouter()
|
||||||
api_router.include_router(login.router, tags=["login"])
|
api_router.include_router(login.router, tags=["login"])
|
||||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||||
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
|
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
|
||||||
api_router.include_router(items.router, prefix="/items", tags=["items"])
|
api_router.include_router(items.router, prefix="/items", tags=["items"])
|
||||||
|
|
||||||
|
|
||||||
|
if settings.ENVIRONMENT == "local":
|
||||||
|
api_router.include_router(private.router)
|
||||||
|
38
backend/app/api/routes/private.py
Normal file
38
backend/app/api/routes/private.py
Normal file
@@ -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
|
26
backend/app/tests/api/routes/test_private.py
Normal file
26
backend/app/tests/api/routes/test_private.py
Normal file
@@ -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"
|
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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 ./",
|
"lint": "biome check --apply-unsafe --no-errors-on-unmatched --files-ignore-unknown=true ./",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate-client": "openapi-ts"
|
"generate-client": "openapi-ts"
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
import { expect, test } from "@playwright/test"
|
||||||
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
|
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
|
||||||
import { randomEmail, randomPassword } from "./utils/random"
|
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"]
|
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.use({ storageState: { cookies: [], origins: [] } })
|
||||||
|
|
||||||
test("Edit user name with a valid name", async ({ page }) => {
|
test("Edit user name with a valid name", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const updatedName = "Test User 2"
|
const updatedName = "Test User 2"
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
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 }) => {
|
test("Edit user email with a valid email", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const updatedEmail = randomEmail()
|
const updatedEmail = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
await logInUser(page, email, password)
|
||||||
@@ -77,13 +74,11 @@ test.describe("Edit user with invalid data", () => {
|
|||||||
test.use({ storageState: { cookies: [], origins: [] } })
|
test.use({ storageState: { cookies: [], origins: [] } })
|
||||||
|
|
||||||
test("Edit user email with an invalid email", async ({ page }) => {
|
test("Edit user email with an invalid email", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const invalidEmail = ""
|
const invalidEmail = ""
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
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 }) => {
|
test("Cancel edit action restores original name", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const updatedName = "Test User"
|
const updatedName = "Test User"
|
||||||
|
|
||||||
// Sign up a new user
|
const user = await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
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.getByLabel("Full name").fill(updatedName)
|
||||||
await page.getByRole("button", { name: "Cancel" }).first().click()
|
await page.getByRole("button", { name: "Cancel" }).first().click()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByLabel("My profile").getByText(fullName, { exact: true }),
|
page
|
||||||
|
.getByLabel("My profile")
|
||||||
|
.getByText(user.full_name as string, { exact: true }),
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Cancel edit action restores original email", async ({ page }) => {
|
test("Cancel edit action restores original email", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const updatedEmail = randomEmail()
|
const updatedEmail = randomEmail()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
await logInUser(page, email, password)
|
||||||
@@ -147,13 +140,11 @@ test.describe("Change password successfully", () => {
|
|||||||
test.use({ storageState: { cookies: [], origins: [] } })
|
test.use({ storageState: { cookies: [], origins: [] } })
|
||||||
|
|
||||||
test("Update password successfully", async ({ page }) => {
|
test("Update password successfully", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const NewPassword = randomPassword()
|
const NewPassword = randomPassword()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
await logInUser(page, email, password)
|
||||||
@@ -177,13 +168,11 @@ test.describe("Change password with invalid data", () => {
|
|||||||
test.use({ storageState: { cookies: [], origins: [] } })
|
test.use({ storageState: { cookies: [], origins: [] } })
|
||||||
|
|
||||||
test("Update password with weak passwords", async ({ page }) => {
|
test("Update password with weak passwords", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const weakPassword = "weak"
|
const weakPassword = "weak"
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
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 ({
|
test("New password and confirmation password do not match", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
const newPassword = randomPassword()
|
const newPassword = randomPassword()
|
||||||
const confirmPassword = randomPassword()
|
const confirmPassword = randomPassword()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
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 }) => {
|
test("Current password and new password are the same", async ({ page }) => {
|
||||||
const fullName = "Test User"
|
|
||||||
const email = randomEmail()
|
const email = randomEmail()
|
||||||
const password = randomPassword()
|
const password = randomPassword()
|
||||||
|
|
||||||
// Sign up a new user
|
await createUser({ email, password })
|
||||||
await signUpNewUser(page, fullName, email, password)
|
|
||||||
|
|
||||||
// Log in the user
|
// Log in the user
|
||||||
await logInUser(page, email, password)
|
await logInUser(page, email, password)
|
||||||
|
22
frontend/tests/utils/privateApi.ts
Normal file
22
frontend/tests/utils/privateApi.ts
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
4
frontend/tsconfig.build.json
Normal file
4
frontend/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["tests/**/*.ts"]
|
||||||
|
}
|
Reference in New Issue
Block a user