Skip to content

Commit

Permalink
✨ Add endpoint to create an app (#202)
Browse files Browse the repository at this point in the history
  • Loading branch information
alejsdev authored Aug 14, 2024
1 parent 109f86e commit bc28b09
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 19 deletions.
3 changes: 2 additions & 1 deletion backend/app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter

from app.api.routes import invitations, login, teams, users, utils
from app.api.routes import apps, invitations, login, teams, users, utils

api_router = APIRouter()
api_router.include_router(login.router, tags=["login"])
Expand All @@ -10,3 +10,4 @@
api_router.include_router(
invitations.router, prefix="/invitations", tags=["invitations"]
)
api_router.include_router(apps.router, prefix="/apps", tags=["apps"])
37 changes: 37 additions & 0 deletions backend/app/api/routes/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import Any

from fastapi import APIRouter, HTTPException

from app.api.deps import CurrentUser, SessionDep
from app.api.utils.teams import generate_app_slug_name
from app.crud import get_user_team_link_by_user_id_and_team_slug
from app.models import (
App,
AppCreate,
AppPublic,
)

router = APIRouter()


@router.post("/", response_model=AppPublic)
def create_app(
session: SessionDep, current_user: CurrentUser, app_in: AppCreate
) -> Any:
"""
Create a new app with the provided details.
"""
user_team_link = get_user_team_link_by_user_id_and_team_slug(
session=session, user_id=current_user.id, team_slug=app_in.team_slug
)
if not user_team_link:
raise HTTPException(
status_code=404, detail="Team not found for the current user"
)
team = user_team_link.team
app_slug = generate_app_slug_name(app_in.name, session)
app = App.model_validate(app_in, update={"slug": app_slug, "team_id": team.id})
session.add(app)
session.commit()
session.refresh(app)
return app
4 changes: 2 additions & 2 deletions backend/app/api/routes/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlmodel import col, func, select

from app.api.deps import CurrentUser, SessionDep, get_current_user
from app.api.utils.teams import verify_and_generate_slug_name
from app.api.utils.teams import generate_team_slug_name
from app.models import (
Message,
Role,
Expand Down Expand Up @@ -92,7 +92,7 @@ def create_team(
"""
Create a new team with the provided details.
"""
team_slug = verify_and_generate_slug_name(team_in.name, session)
team_slug = generate_team_slug_name(team_in.name, session)

team = Team.model_validate(
team_in, update={"slug": team_slug, "owner_id": current_user.id}
Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/routes/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from app import crud
from app.api.deps import CurrentUser, SessionDep, get_first_superuser
from app.api.utils.teams import verify_and_generate_slug_name
from app.api.utils.teams import generate_team_slug_name
from app.core.security import get_password_hash, verify_password
from app.models import (
EmailVerificationToken,
Expand Down Expand Up @@ -205,7 +205,7 @@ def verify_email_token(session: SessionDep, payload: EmailVerificationToken) ->
if user.is_verified:
raise HTTPException(status_code=400, detail="Email already verified")

team_slug = verify_and_generate_slug_name(session=session, name=user.username)
team_slug = generate_team_slug_name(session=session, name=user.username)
team = Team(name=user.full_name, slug=team_slug, owner=user, is_personal_team=True)
user_team_link = UserTeamLink(team=team, user=user, role=Role.admin)

Expand Down
12 changes: 10 additions & 2 deletions backend/app/api/utils/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

from sqlmodel import Session, select

from app.models import Team
from app.models import App, Team
from app.utils import slugify


def verify_and_generate_slug_name(name: str, session: Session) -> str:
def generate_team_slug_name(name: str, session: Session) -> str:
slug_name = slugify(name)
while session.exec(select(Team).where(Team.slug == slug_name)).first():
slug_name = f"{slug_name}-{secrets.token_hex(4)}"

return slug_name


def generate_app_slug_name(name: str, session: Session) -> str:
slug_name = slugify(name)
while session.exec(select(App).where(App.slug == slug_name)).first():
slug_name = f"{slug_name}-{secrets.token_hex(4)}"

return slug_name
21 changes: 18 additions & 3 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class TeamUpdate(SQLModel):

class TeamPublic(TeamBase):
id: uuid.UUID
slug: str = Field(default=None, max_length=255)
slug: str = Field(max_length=255)
is_personal_team: bool
owner_id: uuid.UUID

Expand Down Expand Up @@ -255,9 +255,12 @@ class Invitation(InvitationBase, table=True):
team: Team = Relationship(back_populates="invitations")


class App(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
class AppBase(SQLModel):
name: str = Field(max_length=255, min_length=1)


class App(AppBase, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
team_id: uuid.UUID = Field(foreign_key="team.id")
team: Team = Relationship(back_populates="apps")
slug: str = Field(max_length=255, unique=True)
Expand All @@ -268,6 +271,18 @@ class App(SQLModel, table=True):
)


class AppCreate(AppBase):
team_slug: str


class AppPublic(AppBase):
id: uuid.UUID
team_id: uuid.UUID
slug: str
created_at: datetime
updated_at: datetime


class DeploymentStatus(str, Enum):
waiting_upload = "waiting_upload"
building = "building"
Expand Down
143 changes: 143 additions & 0 deletions backend/app/tests/api/routes/test_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from fastapi.testclient import TestClient
from sqlmodel import Session, select

from app.core.config import settings
from app.crud import add_user_to_team
from app.models import App, Role
from app.tests.utils.team import create_random_team
from app.tests.utils.user import create_user, user_authentication_headers
from app.tests.utils.utils import random_email


def test_create_app_admin(client: TestClient, db: Session) -> None:
user = create_user(
session=db,
email=random_email(),
password="password12345",
full_name="Admin User",
is_verified=True,
)
team = create_random_team(db)
add_user_to_team(session=db, user=user, team=team, role=Role.admin)

user_auth_headers = user_authentication_headers(
client=client,
email=user.email,
password="password12345",
)

app_in = {"name": "test app", "team_slug": team.slug}
response = client.post(
f"{settings.API_V1_STR}/apps/",
headers=user_auth_headers,
json=app_in,
)

assert response.status_code == 200
data = response.json()
assert data["id"]
assert data["name"] == app_in["name"]
assert data["team_id"] == str(team.id)

app_query = select(App).where(App.id == data["id"])
app_db = db.exec(app_query).first()
assert app_db
assert app_db.name == app_in["name"]
assert app_db.team_id == team.id


def test_create_app_member(client: TestClient, db: Session) -> None:
user = create_user(
session=db,
email=random_email(),
password="password12345",
full_name="Member User",
is_verified=True,
)
team = create_random_team(db)
add_user_to_team(session=db, user=user, team=team, role=Role.member)

user_auth_headers = user_authentication_headers(
client=client,
email=user.email,
password="password12345",
)

app_in = {"name": "test app", "team_slug": team.slug}
response = client.post(
f"{settings.API_V1_STR}/apps/",
headers=user_auth_headers,
json=app_in,
)

assert response.status_code == 200
data = response.json()
assert data["id"]
assert data["name"] == app_in["name"]
assert data["team_id"] == str(team.id)

app_query = select(App).where(App.id == data["id"])
app_db = db.exec(app_query).first()
assert app_db
assert app_db.name == app_in["name"]
assert app_db.team_id == team.id


def test_create_app_with_empty_name(client: TestClient, db: Session) -> None:
user = create_user(
session=db,
email=random_email(),
password="password12345",
full_name="Test User",
is_verified=True,
)
team = create_random_team(db, owner_id=user.id)

user_auth_headers = user_authentication_headers(
client=client,
email=user.email,
password="password12345",
)

app_in = {"name": "", "team_slug": team.slug}
response = client.post(
f"{settings.API_V1_STR}/apps/",
headers=user_auth_headers,
json=app_in,
)

assert response.status_code == 422

data = response.json()

assert data["detail"][0]["loc"] == ["body", "name"]
assert data["detail"][0]["msg"] == "String should have at least 1 character"


def test_create_project_user_not_in_team(client: TestClient, db: Session) -> None:
user = create_user(
session=db,
email=random_email(),
password="password12345",
full_name="Test User",
is_verified=True,
)
team = create_random_team(db)

user_auth_headers = user_authentication_headers(
client=client,
email=user.email,
password="password12345",
)

app_in = {"name": "test app", "team_slug": team.slug}
response = client.post(
f"{settings.API_V1_STR}/apps/",
headers=user_auth_headers,
json=app_in,
)

assert response.status_code == 404

data = response.json()
assert data["detail"] == "Team not found for the current user"
14 changes: 5 additions & 9 deletions backend/app/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from app.core.config import settings
from app.core.db import engine, init_db
from app.main import app
from app.models import Invitation, Team, User, UserTeamLink
from app.models import App, Deployment, Invitation, Team, User, UserTeamLink
from app.tests.utils.user import authentication_token_from_email
from app.tests.utils.utils import get_superuser_token_headers

Expand All @@ -22,14 +22,10 @@ def db() -> Generator[Session, None, None]:
try:
yield session
finally:
statement = delete(UserTeamLink)
session.exec(statement) # type: ignore
statement = delete(Invitation)
session.exec(statement) # type: ignore
statement = delete(User)
session.exec(statement) # type: ignore
statement = delete(Team)
session.exec(statement) # type: ignore
models = [UserTeamLink, Invitation, Deployment, App, User, Team]
for model in models:
statement = delete(model)
session.exec(statement) # type: ignore
session.commit()


Expand Down

0 comments on commit bc28b09

Please sign in to comment.