From 75f0e09cc3204aebdf7e07dfb7d77249d963b708 Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 18:47:29 +0200 Subject: [PATCH 01/12] fixed tests and created ci/cd pipeline --- .github/workflows/github-action.yaml | 53 ++++++++++++++++++++++++++++ alembic.ini | 2 +- alembic/env.py | 13 +++---- blogpost.py | 8 +---- database.py | 25 +++++-------- oauth2.py | 8 ++--- routers/posts.py | 6 ++-- schemas.py | 11 +++--- config.py => settings.py | 5 ++- tests/conftest.py | 32 +++++++++-------- tests/test_user.py | 22 +++++------- 11 files changed, 110 insertions(+), 75 deletions(-) create mode 100644 .github/workflows/github-action.yaml rename config.py => settings.py (77%) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml new file mode 100644 index 0000000..e0817c3 --- /dev/null +++ b/.github/workflows/github-action.yaml @@ -0,0 +1,53 @@ +name: CI/CD Pipeline for Blogpost + +on: + push: + branches: + - '*' + pull_request: + branches: + - main + +jobs: + Development-Tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest flake8 + + - name: Set environment variables for testing + run: | + echo "ENVIRONMENT=development" >> $GITHUB_ENV + echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV + echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV + echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV + echo "DB_PORT=${{ secrets.DB_PORT }}" >> $GITHUB_ENV + echo "DB_NAME=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV + echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV + echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV + echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV + - name: Build ALEMBIC_DB_URL + run: | + echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${DB_NAME}" >> $GITHUB_ENV + - name: Run linting + run: | + flake8 . --max-line-length=88 --extend-ignore=E203 + + - name: Run tests + run: | + pytest tests/ +# - name: Run development server +# run: | +# gunicorn -w 4 -k uvicorn.workers.UvicornWorker blogpost:app \ No newline at end of file diff --git a/alembic.ini b/alembic.ini index 54f756d..d984541 100644 --- a/alembic.ini +++ b/alembic.ini @@ -84,7 +84,7 @@ path_separator = os # database URL. This is consumed by the user-maintained env.py script only. # other means of configuring database URLs may be customized within the env.py # file. -sqlalchemy.url = driver://user:pass@localhost/dbname +sqlalchemy.url = driver://user:pass@localhost:port/dbname [post_write_hooks] diff --git a/alembic/env.py b/alembic/env.py index 54540fc..04f8c0e 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -2,19 +2,20 @@ from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context -from database import DATABASE_URL from models import Base -from tests.conftest import TEST_DATABASE_URL +import os # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -# provide database URL for database name, user, password, host, and port -config.set_main_option("sqlalchemy.url", DATABASE_URL) +# Read database URL from env vars +db_url = os.getenv("ALEMBIC_DB_URL") +if not db_url: + raise RuntimeError("ALEMBIC_DB_URL environment variable is not set") + +config.set_main_option("sqlalchemy.url", db_url) -# provide database URL for testing environment -#config.set_main_option("sqlalchemy.url", TEST_DATABASE_URL) # Interpret the config file for Python logging. # This line sets up loggers basically. diff --git a/blogpost.py b/blogpost.py index 93e2aea..dcae349 100644 --- a/blogpost.py +++ b/blogpost.py @@ -2,8 +2,6 @@ from routers import vote, users, auth, posts from fastapi.middleware.cors import CORSMiddleware # import models -# from database import engine - #models.Base.metadata.create_all(bind=engine) @@ -22,8 +20,4 @@ app.include_router(posts.router) app.include_router(users.router) app.include_router(auth.router) -app.include_router(vote.router) - -@app.get("/") -def root(): - return {'message': 'Hello world!'} \ No newline at end of file +app.include_router(vote.router) \ No newline at end of file diff --git a/database.py b/database.py index d6c0626..9ed526a 100644 --- a/database.py +++ b/database.py @@ -1,16 +1,19 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from config import settings -# import psycopg2 -# from psycopg2.extras import RealDictCursor -# import time +from settings import Setting +import os + # database URL set up -DATABASE_URL = f"postgresql://{settings.db_username}:{settings.db_password}@{settings.db_hostname}/{settings.db_name}" +DATABASE_URL = f"postgresql://{Setting.db_username}:{Setting.db_password}@{Setting.db_hostname}:{Setting.db_port}/{Setting.db_name}" +os.environ["ALEMBIC_DB_URL"] = DATABASE_URL engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, bind=engine, autoflush=False) +# Base class for models +Base = declarative_base() + def get_db(): db = SessionLocal() try: @@ -18,15 +21,3 @@ def get_db(): finally: db.close() -Base = declarative_base() - -# while True: -# try: -# conn = psycopg2.connect(host='localhost', database='fastapi', user='postgres', password='mehar12', -# cursor_factory=RealDictCursor) -# cursor = conn.cursor() -# print("Database connection was successful!") -# break -# except Exception as e: -# print("Failed database server connection:", e) -# time.sleep(2) diff --git a/oauth2.py b/oauth2.py index 2904fca..246d27c 100644 --- a/oauth2.py +++ b/oauth2.py @@ -5,13 +5,13 @@ from fastapi import Depends, status, HTTPException from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session -from config import settings +from settings import Setting oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") -SECRET_KEY = settings.secret_key -ALGORITHM = settings.algorithm -ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes +SECRET_KEY = Setting.secret_key +ALGORITHM = Setting.algorithm +ACCESS_TOKEN_EXPIRE_MINUTES = Setting.access_token_expire_minutes def create_access_token(data: dict): to_encode = data.copy() diff --git a/routers/posts.py b/routers/posts.py index 757f14b..fd79f3a 100644 --- a/routers/posts.py +++ b/routers/posts.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from typing import List, Optional -from fastapi import status, HTTPException, Response, Depends, APIRouter +from fastapi import status, HTTPException, Depends, APIRouter import schemas, models, oauth2 from database import get_db from sqlalchemy import func @@ -59,12 +59,12 @@ def delete_posts(post_id: int, db: Session = Depends(get_db), if post is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with id: {post_id} is not found.") - if post.id != current_user.id: + if post.user_id != current_user.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to perform action!") db.delete(post) db.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) + return {'message': f'Post {post_id} deleted successfully.'} @router.put('/{post_id}', response_model=schemas.ResponsePost) def update_posts(post_id: int, new_post: schemas.CreatePost, db: Session = Depends(get_db), diff --git a/schemas.py b/schemas.py index c920451..d3acf39 100644 --- a/schemas.py +++ b/schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, ConfigDict from datetime import datetime from typing import Optional from pydantic.types import conint @@ -13,8 +13,7 @@ class ResponseUser(BaseModel): id: int email: EmailStr created_at: datetime - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class ResponsePost(BaseModel): id: int @@ -24,14 +23,12 @@ class ResponsePost(BaseModel): created_at: datetime user_id: int user: ResponseUser - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class ResponsePostVote(BaseModel): Post: ResponsePost votes: int - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class CreateUser(BaseModel): email: EmailStr diff --git a/config.py b/settings.py similarity index 77% rename from config.py rename to settings.py index a9f31aa..2534960 100644 --- a/config.py +++ b/settings.py @@ -11,9 +11,12 @@ class ConfigSettings(BaseSettings): db_name: str secret_key: str algorithm: str + environment: str + test_db_name: str + alembic_database_url: str access_token_expire_minutes: int class Config: env_file = ".env" -settings = ConfigSettings() \ No newline at end of file +Setting = ConfigSettings() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 8b185e1..dac6e1b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,28 +1,26 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from config import settings +from settings import Setting from alembic.config import Config from alembic import command import pytest from blogpost import app -from database import get_db +from database import get_db, Base from fastapi.testclient import TestClient from oauth2 import create_access_token import models +import logging, sys, os +logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(levelname)s:%(name)s:%(message)s') +logger = logging.getLogger(__name__) -TEST_DATABASE_URL = f"postgresql://{settings.db_username}:{settings.db_password}@{settings.db_hostname}/{settings.db_name}_test" + +TEST_DATABASE_URL = f"postgresql://{Setting.db_username}:{Setting.db_password}@{Setting.db_hostname}:{Setting.db_port}/{Setting.test_db_name}" +os.environ["ALEMBIC_DB_URL"] = TEST_DATABASE_URL test_engine = create_engine(TEST_DATABASE_URL) TestSessionLocal = sessionmaker(autocommit=False, bind=test_engine, autoflush=False) -def override_get_db(): - db = TestSessionLocal() - try: - yield db - finally: - db.close() - @pytest.fixture def session(): @@ -48,18 +46,22 @@ def override_get_db(): @pytest.fixture -def test_user(client): - user_data = {'email': 'testcarla@gmail.com', 'password': 'carla12'} +def test_user(client, session): + user_data = {'email': 'test_user_1@gmail.com', 'password': 'mypass12'} response = client.post('/users/', json=user_data) assert response.status_code == 201 new_user = response.json() new_user['password'] = user_data['password'] + + # Verify data in session + db_user = session.query(models.User).filter_by(email=user_data['email']).first() + assert db_user, "User not created in database" return new_user @pytest.fixture -def test_user2(client): - user_data = {'email': 'testdevil@gmail.com', 'password': 'carla12'} +def test_user2(client, session): + user_data = {'email': 'test_user_2@gmail.com', 'password': 'mypass12'} response = client.post('/users/', json=user_data) assert response.status_code == 201 @@ -68,7 +70,7 @@ def test_user2(client): return new_user @pytest.fixture -def token(test_user): +def token(test_user, session): return create_access_token({'user_id': test_user['id']}) @pytest.fixture diff --git a/tests/test_user.py b/tests/test_user.py index f35607c..0259cf7 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,13 +1,8 @@ import schemas import jwt -from config import settings +from settings import Setting import pytest -def test_root(client): - response = client.get("/") - assert response.status_code == 200 - assert response.json().get('message') == 'Hello world!' - def test_create_user(client): response = client.post('/users/', json={'email': 'testcarla12@gmail.com', 'password': 'carla12'}) @@ -19,7 +14,7 @@ def test_login_user(client, test_user): response = client.post('/login/', data={'username': test_user['email'], 'password': test_user['password']}) token = schemas.Token(**response.json()) - payload = jwt.decode(token.access_token, settings.secret_key, algorithms=[settings.algorithm]) + payload = jwt.decode(token.access_token, Setting.secret_key, algorithms=[Setting.algorithm]) id: str = payload.get('user_id') assert response.status_code == 200 assert token.token_type == 'Bearer' @@ -28,18 +23,17 @@ def test_login_user(client, test_user): def test_get_user(client, test_user): response = client.get('/users/1') user = schemas.ResponseUser(**response.json()) - assert user.email == 'testcarla@gmail.com' + assert user.email == 'test_user_1@gmail.com' assert response.status_code == 200 @pytest.mark.parametrize('email, password, status_code', [ - ('wrongemail@gmail.com', 'carla12', 403), - ('testcarla@gmail.com', 'wrong password', 403), - (None, 'carla12', 403), - ('testcarla@gmail.com', None, 403) + ('wrongemail@gmail.com', 'mypass12', 403), + ('test_user_1@gmail.com', 'wrong password', 403), + (None, 'mypass12', 403), + ('test_user_1@gmail.com', None, 403) ]) def test_user_failed_login(client, test_user, email, password, status_code): response = client.post('/login/', data={'username': email, 'password': password}) - assert response.status_code == status_code - #assert response.json().get('details') == 'Invalid credentials' \ No newline at end of file + assert response.status_code == status_code \ No newline at end of file From b9f4ca05f879c4d4c6a2ba569f5f188f56cf34f0 Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 22:35:08 +0200 Subject: [PATCH 02/12] formatted the code files and added docker image for postgresql database in action file to run tests --- .flake8 | 11 +++++ .github/workflows/github-action.yaml | 19 ++++++- alembic/env.py | 2 +- .../a68d3c1313e9_create_all_tables.py | 49 ++++++++++--------- blogpost.py | 5 +- database.py | 10 ++-- models.py | 20 +++++--- oauth2.py | 8 +-- routers/auth.py | 11 +++-- routers/posts.py | 27 ++++++---- routers/users.py | 9 ++-- routers/vote.py | 14 ++++-- schemas.py | 8 +++ settings.py | 4 +- tests/conftest.py | 18 +++++-- tests/test_post.py | 18 +++++-- tests/test_user.py | 5 +- tests/test_vote.py | 9 +++- utils.py | 4 +- 19 files changed, 174 insertions(+), 77 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f0c7856 --- /dev/null +++ b/.flake8 @@ -0,0 +1,11 @@ +# .flake8 +[flake8] +max-line-length = 98 +extend-ignore = E203 +exclude = + .git, + __pycache__, + .venv, + env, + venv, + alembic/versions diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index e0817c3..783a887 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -12,6 +12,21 @@ jobs: Development-Tests: runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: ${{ secrets.DB_USERNAME }} + POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }} + POSTGRES_DB: ${{ secrets.TEST_DB_NAME }} + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: - name: Checkout code uses: actions/checkout@v4 @@ -43,11 +58,11 @@ jobs: echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${DB_NAME}" >> $GITHUB_ENV - name: Run linting run: | - flake8 . --max-line-length=88 --extend-ignore=E203 + flake8 --verbose - name: Run tests run: | - pytest tests/ + pytest tests/ -v # - name: Run development server # run: | # gunicorn -w 4 -k uvicorn.workers.UvicornWorker blogpost:app \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py index 04f8c0e..c58c480 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -16,7 +16,6 @@ config.set_main_option("sqlalchemy.url", db_url) - # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: @@ -29,6 +28,7 @@ # target_metadata = mymodel.Base.metadata target_metadata = Base.metadata + # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") diff --git a/alembic/versions/a68d3c1313e9_create_all_tables.py b/alembic/versions/a68d3c1313e9_create_all_tables.py index d56f513..81e4838 100644 --- a/alembic/versions/a68d3c1313e9_create_all_tables.py +++ b/alembic/versions/a68d3c1313e9_create_all_tables.py @@ -10,7 +10,6 @@ from alembic import op import sqlalchemy as sa - # revision identifiers, used by Alembic. revision: str = 'a68d3c1313e9' down_revision: Union[str, Sequence[str], None] = None @@ -22,31 +21,33 @@ def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('password', sa.String(), nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), + nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) op.create_table('posts', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('title', sa.String(), nullable=False), - sa.Column('content', sa.String(), nullable=False), - sa.Column('is_published', sa.Boolean(), server_default='TRUE', nullable=False), - sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('title') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('content', sa.String(), nullable=False), + sa.Column('is_published', sa.Boolean(), server_default='TRUE', nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), + nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('title') + ) op.create_table('votes', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('post_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'post_id') - ) + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('post_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'post_id') + ) # ### end Alembic commands ### diff --git a/blogpost.py b/blogpost.py index dcae349..fe270f9 100644 --- a/blogpost.py +++ b/blogpost.py @@ -1,9 +1,10 @@ from fastapi import FastAPI from routers import vote, users, auth, posts from fastapi.middleware.cors import CORSMiddleware + # import models -#models.Base.metadata.create_all(bind=engine) +# models.Base.metadata.create_all(bind=engine) app = FastAPI() @@ -20,4 +21,4 @@ app.include_router(posts.router) app.include_router(users.router) app.include_router(auth.router) -app.include_router(vote.router) \ No newline at end of file +app.include_router(vote.router) diff --git a/database.py b/database.py index 9ed526a..9a0b232 100644 --- a/database.py +++ b/database.py @@ -1,12 +1,14 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker from settings import Setting import os - # database URL set up -DATABASE_URL = f"postgresql://{Setting.db_username}:{Setting.db_password}@{Setting.db_hostname}:{Setting.db_port}/{Setting.db_name}" +DATABASE_URL = ( + f"postgresql://{Setting.db_username}:{Setting.db_password}" + f"@{Setting.db_hostname}:{Setting.db_port}/{Setting.db_name}" +) os.environ["ALEMBIC_DB_URL"] = DATABASE_URL engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, bind=engine, autoflush=False) @@ -14,10 +16,10 @@ # Base class for models Base = declarative_base() + def get_db(): db = SessionLocal() try: yield db finally: db.close() - diff --git a/models.py b/models.py index 86c5722..38750fe 100644 --- a/models.py +++ b/models.py @@ -11,8 +11,10 @@ class Post(Base): title = Column(String, nullable=False, unique=True) content = Column(String, nullable=False) is_published = Column(Boolean, nullable=False, server_default='TRUE') - created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()')) - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, + server_default=text('now()')) + user_id = Column(Integer, ForeignKey("users.id", + ondelete="CASCADE"), nullable=False) user = relationship("User") @@ -22,11 +24,15 @@ class User(Base): id = Column(Integer, primary_key=True, nullable=False) email = Column(String, nullable=False, unique=True) password = Column(String, nullable=False) - created_at = Column(TIMESTAMP(timezone=True), nullable=False, server_default=text('now()')) + created_at = Column(TIMESTAMP(timezone=True), nullable=False, + server_default=text('now()')) + class Vote(Base): __tablename__ = 'votes' - user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, - primary_key=True) - post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), nullable=False, - primary_key=True) \ No newline at end of file + user_id = Column(Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, primary_key=True) + post_id = Column(Integer, + ForeignKey("posts.id", ondelete="CASCADE"), + nullable=False, primary_key=True) diff --git a/oauth2.py b/oauth2.py index 246d27c..892971c 100644 --- a/oauth2.py +++ b/oauth2.py @@ -7,12 +7,12 @@ from sqlalchemy.orm import Session from settings import Setting - oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") SECRET_KEY = Setting.secret_key ALGORITHM = Setting.algorithm ACCESS_TOKEN_EXPIRE_MINUTES = Setting.access_token_expire_minutes + def create_access_token(data: dict): to_encode = data.copy() expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) @@ -20,6 +20,7 @@ def create_access_token(data: dict): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt + def varify_access_token(token: str, credential_exception): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) @@ -32,10 +33,11 @@ def varify_access_token(token: str, credential_exception): return token_data -def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)): +def get_current_user(token: str = Depends(oauth2_scheme), + db: Session = Depends(database.get_db)): credential_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}) token = varify_access_token(token, credential_exception) user = db.query(models.User).filter(models.User.id == token.id).first() - return user \ No newline at end of file + return user diff --git a/routers/auth.py b/routers/auth.py index 85ef08e..42aff23 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -4,15 +4,18 @@ import schemas, models, utils, oauth2 from fastapi.security.oauth2 import OAuth2PasswordRequestForm - router = APIRouter(tags=['Authentication']) + @router.post("/login", response_model=schemas.Token) -def login(user_credential: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): +def login(user_credential: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db)): user = db.query(models.User).filter(models.User.email == user_credential.username).first() if user is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid credentials") if not utils.varify_password(user_credential.password, user.password): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid credentials") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Invalid credentials") access_token = oauth2.create_access_token(data={'user_id': user.id}) return {'access_token': access_token, 'token_type': 'Bearer'} diff --git a/routers/posts.py b/routers/posts.py index fd79f3a..185acaa 100644 --- a/routers/posts.py +++ b/routers/posts.py @@ -5,29 +5,33 @@ from database import get_db from sqlalchemy import func - router = APIRouter(prefix="/posts", tags=['Posts']) + @router.get('/user', response_model=List[schemas.ResponsePost]) -def get_user_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)): +def get_user_posts(db: Session = Depends(get_db), + current_user: int = Depends(oauth2.get_current_user)): posts = db.query(models.Post).filter(models.Post.user_id == current_user.id).all() return posts + @router.get('/', response_model=List[schemas.ResponsePostVote]) -def get_all_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user), - limit: int = 5, skip: int = 0, search: Optional[str] = ""): - #posts = db.query(models.Post).filter(models.Post.title.contains(search)).limit(limit).offset(skip).all() +def get_all_posts(db: Session = Depends(get_db), + current_user: int = Depends(oauth2.get_current_user), + search: Optional[str] = ""): result = db.query( models.Post, func.count(models.Vote.post_id)).join( - models.Vote, models.Post.id == models.Vote.post_id,isouter=True).filter( + models.Vote, models.Post.id == models.Vote.post_id, isouter=True).filter( models.Post.title.contains(search)).group_by(models.Post.id).all() response = [{"Post": row[0], "votes": row[1]} for row in result] return response -@router.post('/', status_code=status.HTTP_201_CREATED, response_model=schemas.ResponsePost) + +@router.post('/', status_code=status.HTTP_201_CREATED, + response_model=schemas.ResponsePost) def create_new_post(post: schemas.CreatePost, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)): - new_post = models.Post(user_id = current_user.id, **post.model_dump()) + new_post = models.Post(user_id=current_user.id, **post.model_dump()) db.add(new_post) db.commit() db.refresh(new_post) @@ -66,14 +70,17 @@ def delete_posts(post_id: int, db: Session = Depends(get_db), db.commit() return {'message': f'Post {post_id} deleted successfully.'} + @router.put('/{post_id}', response_model=schemas.ResponsePost) def update_posts(post_id: int, new_post: schemas.CreatePost, db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)): post = db.query(models.Post).filter(models.Post.id == post_id).first() if post is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"post with id: {post_id} is not found.") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"post with id: {post_id} is not found.") if post.id != current_user.id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to perform action!") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="Not authorized to perform action!") post.title = new_post.title post.content = new_post.content diff --git a/routers/users.py b/routers/users.py index 04c5f9b..99caf34 100644 --- a/routers/users.py +++ b/routers/users.py @@ -4,11 +4,13 @@ from database import get_db from sqlalchemy.orm import Session - router = APIRouter(prefix="/users", tags=['Users']) -@router.post('/', status_code=status.HTTP_201_CREATED, response_model=schemas.ResponseUser) -def create_user(user: schemas.CreateUser, db: Session = Depends(get_db)): + +@router.post('/', status_code=status.HTTP_201_CREATED, + response_model=schemas.ResponseUser) +def create_user(user: schemas.CreateUser, + db: Session = Depends(get_db)): hashed_password = hash_password(user.password) user.password = hashed_password new_user = models.User(**user.model_dump()) @@ -17,6 +19,7 @@ def create_user(user: schemas.CreateUser, db: Session = Depends(get_db)): db.refresh(new_user) return new_user + @router.get('/{user_id}', response_model=schemas.ResponseUser) def get_user(user_id: int, db: Session = Depends(get_db)): user = db.query(models.User).filter(models.User.id == user_id).first() diff --git a/routers/vote.py b/routers/vote.py index bf700dd..ce1aa22 100644 --- a/routers/vote.py +++ b/routers/vote.py @@ -4,27 +4,31 @@ router = APIRouter(prefix="/vote", tags=['Vote']) + @router.post('/', status_code=status.HTTP_201_CREATED) def user_vote(vote: schemas.UserVote, db: Session = Depends(database.get_db), current_user: int = Depends(oauth2.get_current_user)): post = db.query(models.Post).filter(models.Post.id == vote.post_id).first() # check if post does not exist if post is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Post {vote.post_id} does not exist") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail=f"Post {vote.post_id} does not exist") found_vote = db.query(models.Vote).filter(models.Vote.post_id == vote.post_id, models.Vote.user_id == current_user.id).first() # check if user already voted if vote.dir == 1: if found_vote: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, - detail=f"User {current_user.id} has already voted on post {vote.post_id}") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"User {current_user.id} has already voted on post {vote.post_id}") new_vote = models.Vote(post_id=vote.post_id, user_id=current_user.id) db.add(new_vote) db.commit() return {'message': 'Vote added successfully.'} else: if not found_vote: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vote does not exist") + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail="Vote does not exist") db.delete(found_vote) db.commit() - return {'message': 'Vote deleted successfully.'} \ No newline at end of file + return {'message': 'Vote deleted successfully.'} diff --git a/schemas.py b/schemas.py index d3acf39..440de29 100644 --- a/schemas.py +++ b/schemas.py @@ -9,12 +9,14 @@ class CreatePost(BaseModel): content: str is_published: bool = True + class ResponseUser(BaseModel): id: int email: EmailStr created_at: datetime model_config = ConfigDict(from_attributes=True) + class ResponsePost(BaseModel): id: int title: str @@ -25,26 +27,32 @@ class ResponsePost(BaseModel): user: ResponseUser model_config = ConfigDict(from_attributes=True) + class ResponsePostVote(BaseModel): Post: ResponsePost votes: int model_config = ConfigDict(from_attributes=True) + class CreateUser(BaseModel): email: EmailStr password: str + class UserLogin(BaseModel): email: EmailStr password: str + class Token(BaseModel): access_token: str token_type: str + class TokenData(BaseModel): id: Optional[str] = None + class UserVote(BaseModel): post_id: int dir: conint(le=1) diff --git a/settings.py b/settings.py index 2534960..693f41c 100644 --- a/settings.py +++ b/settings.py @@ -3,6 +3,7 @@ load_dotenv() + class ConfigSettings(BaseSettings): db_hostname: str db_port: str @@ -19,4 +20,5 @@ class ConfigSettings(BaseSettings): class Config: env_file = ".env" -Setting = ConfigSettings() \ No newline at end of file + +Setting = ConfigSettings() diff --git a/tests/conftest.py b/tests/conftest.py index dac6e1b..bcb285c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker from settings import Setting from alembic.config import Config from alembic import command @@ -11,11 +11,15 @@ import models import logging, sys, os -logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(levelname)s:%(name)s:%(message)s') +logging.basicConfig(level=logging.INFO, stream=sys.stdout, + format='%(levelname)s:%(name)s:%(message)s') logger = logging.getLogger(__name__) +TEST_DATABASE_URL = ( + f"postgresql://{Setting.db_username}:{Setting.db_password}" + f"@{Setting.db_hostname}:{Setting.db_port}/{Setting.test_db_name}" +) -TEST_DATABASE_URL = f"postgresql://{Setting.db_username}:{Setting.db_password}@{Setting.db_hostname}:{Setting.db_port}/{Setting.test_db_name}" os.environ["ALEMBIC_DB_URL"] = TEST_DATABASE_URL test_engine = create_engine(TEST_DATABASE_URL) @@ -41,6 +45,7 @@ def override_get_db(): yield session finally: session.close() + app.dependency_overrides[get_db] = override_get_db yield TestClient(app) @@ -59,6 +64,7 @@ def test_user(client, session): assert db_user, "User not created in database" return new_user + @pytest.fixture def test_user2(client, session): user_data = {'email': 'test_user_2@gmail.com', 'password': 'mypass12'} @@ -69,15 +75,18 @@ def test_user2(client, session): new_user['password'] = user_data['password'] return new_user + @pytest.fixture def token(test_user, session): return create_access_token({'user_id': test_user['id']}) + @pytest.fixture def authorized_client(client, token): client.headers = {**client.headers, 'Authorization': f"Bearer {token}"} return client + @pytest.fixture def test_posts(test_user, session, test_user2): post_data = [ @@ -86,6 +95,7 @@ def test_posts(test_user, session, test_user2): {'title': 'third title', 'content': 'third content', 'user_id': test_user['id']}, {'title': 'fourth title', 'content': 'fourth content', 'user_id': test_user2['id']} ] + def create_post_model(post): return models.Post(**post) @@ -93,4 +103,4 @@ def create_post_model(post): session.add_all(post_map) session.commit() posts = session.query(models.Post).all() - return posts \ No newline at end of file + return posts diff --git a/tests/test_post.py b/tests/test_post.py index e4cde6c..b530ab0 100644 --- a/tests/test_post.py +++ b/tests/test_post.py @@ -1,6 +1,7 @@ import schemas import pytest + @pytest.mark.parametrize('title, content, is_published', [ ('Testing new title', 'This is my testing new content', True), ('Favorite food', 'I like to Pizza', False) @@ -16,18 +17,21 @@ def test_create_user(authorized_client, test_user, test_posts, title, content, i assert new_post.is_published == is_published assert new_post.user_id == test_user['id'] + def test_unauthorized_user_create_post(client, test_posts, test_user): response = client.post('/posts/', json={'title': 'new title', - 'content': 'new content', - 'is_published': True}) + 'content': 'new content', + 'is_published': True}) assert response.status_code == 401 + def test_get_all_posts(authorized_client, test_posts): response = authorized_client.get("/posts/") all_posts_list = response.json() assert response.status_code == 200 assert len(all_posts_list) == len(test_posts) + def test_get_a_post(authorized_client, test_posts): response = authorized_client.get(f"/posts/{test_posts[0].id}") post = schemas.ResponsePostVote(**response.json()) @@ -36,14 +40,17 @@ def test_get_a_post(authorized_client, test_posts): assert post.Post.content == test_posts[0].content assert response.status_code == 200 + def test_unauthorized_user_get_all_posts(client, test_posts): response = client.get("/posts/") assert response.status_code == 401 + def test_unauthorized_user_get_one_post(client, test_posts): response = client.get(f"/posts/{test_posts[0].id}") assert response.status_code == 401 + def test_get_a_post_not_exist(authorized_client, test_posts): response = authorized_client.get(f"/posts/{123}") print('--' * 8) @@ -55,12 +62,13 @@ def test_update_a_post(authorized_client, test_user, test_posts): data = { 'title': 'updated title', 'content': 'updated content', 'user_id': test_user['id'] } - response = authorized_client.put(f'/posts/{test_posts[0].id}', json= data) + response = authorized_client.put(f'/posts/{test_posts[0].id}', json=data) update_post = schemas.ResponsePost(**response.json()) assert response.status_code == 200 assert update_post.title == 'updated title' assert update_post.content == 'updated content' + def test_update_other_user_post(authorized_client, test_user2, test_posts): data = { 'title': 'updated title', 'content': 'updated content', 'user_id': test_user2['id'] @@ -68,6 +76,7 @@ def test_update_other_user_post(authorized_client, test_user2, test_posts): response = authorized_client.put(f'/posts/{test_posts[3].id}', json=data) assert response.status_code == 403 + def test_user_update_a_post_not_exist(authorized_client, test_user, test_posts): data = { 'title': 'updated title', 'content': 'updated content', 'user_id': test_user['id'] @@ -75,6 +84,7 @@ def test_user_update_a_post_not_exist(authorized_client, test_user, test_posts): response = authorized_client.put(f'/posts/{12}', json=data) assert response.status_code == 404 + def test_unauthorized_user_update_a_post(client, test_posts, test_user): data = { 'title': 'updated title', 'content': 'updated content', 'user_id': test_user['id'] @@ -87,10 +97,12 @@ def test_delete_a_post_success(authorized_client, test_posts, test_user): response = authorized_client.delete(f"/posts/{test_posts[0].id}") assert response.status_code == 204 + def test_user_delete_a_post_not_exist(authorized_client, test_user, test_posts): response = authorized_client.delete(f"/posts/{123}") assert response.status_code == 404 + def test_unauthorized_user_delete_a_post(client, test_user, test_posts): response = client.delete(f"/posts/{test_posts[3].id}") assert response.status_code == 401 diff --git a/tests/test_user.py b/tests/test_user.py index 0259cf7..13e5793 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -3,6 +3,7 @@ from settings import Setting import pytest + def test_create_user(client): response = client.post('/users/', json={'email': 'testcarla12@gmail.com', 'password': 'carla12'}) @@ -10,6 +11,7 @@ def test_create_user(client): assert new_user.email == 'testcarla12@gmail.com' assert response.status_code == 201 + def test_login_user(client, test_user): response = client.post('/login/', data={'username': test_user['email'], 'password': test_user['password']}) @@ -20,6 +22,7 @@ def test_login_user(client, test_user): assert token.token_type == 'Bearer' assert id == test_user['id'] + def test_get_user(client, test_user): response = client.get('/users/1') user = schemas.ResponseUser(**response.json()) @@ -36,4 +39,4 @@ def test_get_user(client, test_user): def test_user_failed_login(client, test_user, email, password, status_code): response = client.post('/login/', data={'username': email, 'password': password}) - assert response.status_code == status_code \ No newline at end of file + assert response.status_code == status_code diff --git a/tests/test_vote.py b/tests/test_vote.py index 3d9ad94..7823b96 100644 --- a/tests/test_vote.py +++ b/tests/test_vote.py @@ -8,28 +8,33 @@ def test_vote(test_user, session, test_posts): session.add(new_vote) session.commit() + def test_vote_on_post(authorized_client, test_posts): - response = authorized_client.post('/vote/', json={'post_id': test_posts[3].id, 'dir':1}) + response = authorized_client.post('/vote/', json={'post_id': test_posts[3].id, 'dir': 1}) assert response.status_code == 201 assert response.json().get('message') == 'Vote added successfully.' + def test_vote_on_post_not_exist(authorized_client, test_posts): response = authorized_client.post('/vote/', json={'post_id': 123, 'dir': 1}) assert response.status_code == 404 + def test_delete_vote_on_post(authorized_client, test_posts, test_vote): response = authorized_client.post('/vote/', json={'post_id': test_posts[3].id, 'dir': 0}) assert response.status_code == 201 + def test_delete_vote_on_post_not_exist(authorized_client, test_posts): response = authorized_client.post('/vote/', json={'post_id': 12, 'dir': 0}) assert response.status_code == 404 + def test_vote_twice_on_post(authorized_client, test_posts, test_vote): response = authorized_client.post('/vote/', json={'post_id': test_posts[3].id, 'dir': 1}) assert response.status_code == 409 + def test_unauthorized_user_vote_on_post(client, test_posts, test_vote): response = client.post('/vote/', json={'post_id': test_posts[3].id, 'dir': 1}) assert response.status_code == 401 - diff --git a/utils.py b/utils.py index dc6b220..5d43680 100644 --- a/utils.py +++ b/utils.py @@ -1,9 +1,11 @@ from passlib.context import CryptContext -pwd_context = CryptContext(schemes=['bcrypt'], deprecated = 'auto') +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + def hash_password(password: str): return pwd_context.hash(password) + def varify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) From 1eb6734364fbf7da75aa75efeb5a4f21f0f1ab4f Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 22:46:05 +0200 Subject: [PATCH 03/12] fixed linting errors in the code files --- oauth2.py | 4 +++- routers/auth.py | 8 ++++++-- routers/posts.py | 7 +++++-- routers/users.py | 6 ++++-- routers/vote.py | 9 +++++++-- tests/conftest.py | 6 ++++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/oauth2.py b/oauth2.py index 892971c..2f3c323 100644 --- a/oauth2.py +++ b/oauth2.py @@ -1,7 +1,9 @@ import jwt from jwt.exceptions import InvalidTokenError from datetime import datetime, timedelta, timezone -import schemas, database, models +import schemas +import database +import models from fastapi import Depends, status, HTTPException from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session diff --git a/routers/auth.py b/routers/auth.py index 42aff23..1c151a7 100644 --- a/routers/auth.py +++ b/routers/auth.py @@ -1,7 +1,11 @@ -from fastapi import APIRouter, Depends, status, HTTPException +from fastapi import APIRouter, Depends +from fastapi import status, HTTPException from sqlalchemy.orm import Session from database import get_db -import schemas, models, utils, oauth2 +import schemas +import models +import utils +import oauth2 from fastapi.security.oauth2 import OAuth2PasswordRequestForm router = APIRouter(tags=['Authentication']) diff --git a/routers/posts.py b/routers/posts.py index 185acaa..0a612e2 100644 --- a/routers/posts.py +++ b/routers/posts.py @@ -1,7 +1,10 @@ from sqlalchemy.orm import Session from typing import List, Optional -from fastapi import status, HTTPException, Depends, APIRouter -import schemas, models, oauth2 +from fastapi import status, HTTPException +from fastapi import Depends, APIRouter +import schemas +import models +import oauth2 from database import get_db from sqlalchemy import func diff --git a/routers/users.py b/routers/users.py index 99caf34..df5cf68 100644 --- a/routers/users.py +++ b/routers/users.py @@ -1,6 +1,8 @@ from utils import hash_password -from fastapi import status, HTTPException, Depends, APIRouter -import schemas, models +from fastapi import status, HTTPException +from fastapi import Depends, APIRouter +import schemas +import models from database import get_db from sqlalchemy.orm import Session diff --git a/routers/vote.py b/routers/vote.py index ce1aa22..774a719 100644 --- a/routers/vote.py +++ b/routers/vote.py @@ -1,6 +1,11 @@ -from fastapi import status, HTTPException, Response, Depends, APIRouter -import schemas, database, models, oauth2 +from fastapi import status, HTTPException +from fastapi import Depends, APIRouter from sqlalchemy.orm import Session +import schemas +import database +import models +import oauth2 + router = APIRouter(prefix="/vote", tags=['Vote']) diff --git a/tests/conftest.py b/tests/conftest.py index bcb285c..6f54e3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,11 +5,13 @@ from alembic import command import pytest from blogpost import app -from database import get_db, Base +from database import get_db from fastapi.testclient import TestClient from oauth2 import create_access_token import models -import logging, sys, os +import logging +import sys +import os logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(levelname)s:%(name)s:%(message)s') From c92c0d3c1885ca636ee29cb8bc0bd77a99c08431 Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 22:57:40 +0200 Subject: [PATCH 04/12] fixed environment variable --- .github/workflows/github-action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index 783a887..b18752c 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -49,13 +49,13 @@ jobs: echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV echo "DB_PORT=${{ secrets.DB_PORT }}" >> $GITHUB_ENV - echo "DB_NAME=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV + echo "TEST_DB_NAME=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV - name: Build ALEMBIC_DB_URL run: | - echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${DB_NAME}" >> $GITHUB_ENV + echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${TEST_DB_NAME}" >> $GITHUB_ENV - name: Run linting run: | flake8 --verbose From dd113650f7eeae55948d70ba80041929a3b9967b Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 23:18:20 +0200 Subject: [PATCH 05/12] fixed environment variables --- .github/workflows/github-action.yaml | 2 +- alembic/env.py | 2 +- database.py | 2 +- settings.py | 4 ++-- tests/conftest.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index b18752c..168753c 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -53,7 +53,7 @@ jobs: echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV - - name: Build ALEMBIC_DB_URL + - name: Build ALEMBIC_DATABASE_URL run: | echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${TEST_DB_NAME}" >> $GITHUB_ENV - name: Run linting diff --git a/alembic/env.py b/alembic/env.py index c58c480..da2667d 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -10,7 +10,7 @@ config = context.config # Read database URL from env vars -db_url = os.getenv("ALEMBIC_DB_URL") +db_url = os.getenv("ALEMBIC_DATABASE_URL") if not db_url: raise RuntimeError("ALEMBIC_DB_URL environment variable is not set") diff --git a/database.py b/database.py index 9a0b232..5c27662 100644 --- a/database.py +++ b/database.py @@ -9,7 +9,7 @@ f"postgresql://{Setting.db_username}:{Setting.db_password}" f"@{Setting.db_hostname}:{Setting.db_port}/{Setting.db_name}" ) -os.environ["ALEMBIC_DB_URL"] = DATABASE_URL +os.environ["ALEMBIC_DATABASE_URL"] = DATABASE_URL engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, bind=engine, autoflush=False) diff --git a/settings.py b/settings.py index 693f41c..400ca4e 100644 --- a/settings.py +++ b/settings.py @@ -8,14 +8,14 @@ class ConfigSettings(BaseSettings): db_hostname: str db_port: str db_password: str - db_username: str db_name: str + db_username: str secret_key: str algorithm: str + access_token_expire_minutes: int environment: str test_db_name: str alembic_database_url: str - access_token_expire_minutes: int class Config: env_file = ".env" diff --git a/tests/conftest.py b/tests/conftest.py index 6f54e3e..d9456ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,7 +22,7 @@ f"@{Setting.db_hostname}:{Setting.db_port}/{Setting.test_db_name}" ) -os.environ["ALEMBIC_DB_URL"] = TEST_DATABASE_URL +os.environ["ALEMBIC_DATABASE_URL"] = TEST_DATABASE_URL test_engine = create_engine(TEST_DATABASE_URL) TestSessionLocal = sessionmaker(autocommit=False, bind=test_engine, autoflush=False) From 61b05dfd3d38acbe65b74fcedc72a6d480769696 Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 23:24:22 +0200 Subject: [PATCH 06/12] check now the tests --- .github/workflows/github-action.yaml | 3 ++- settings.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index 168753c..ce1da25 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -44,7 +44,7 @@ jobs: - name: Set environment variables for testing run: | - echo "ENVIRONMENT=development" >> $GITHUB_ENV + echo "Start Postgresql database set up --" >> $GITHUB_ENV echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV @@ -53,6 +53,7 @@ jobs: echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV + echo "End database set up." >> $GITHUB_ENV - name: Build ALEMBIC_DATABASE_URL run: | echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${TEST_DB_NAME}" >> $GITHUB_ENV diff --git a/settings.py b/settings.py index 400ca4e..80445d2 100644 --- a/settings.py +++ b/settings.py @@ -13,7 +13,6 @@ class ConfigSettings(BaseSettings): secret_key: str algorithm: str access_token_expire_minutes: int - environment: str test_db_name: str alembic_database_url: str From 4f4e5916e8e7ef37a894839f577e061c951b5b9a Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 23:28:39 +0200 Subject: [PATCH 07/12] check again the tests --- .github/workflows/github-action.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index ce1da25..d14582f 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -44,7 +44,7 @@ jobs: - name: Set environment variables for testing run: | - echo "Start Postgresql database set up --" >> $GITHUB_ENV + echo "Start Postgresql database set up" echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV @@ -53,10 +53,8 @@ jobs: echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV - echo "End database set up." >> $GITHUB_ENV - - name: Build ALEMBIC_DATABASE_URL - run: | echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${TEST_DB_NAME}" >> $GITHUB_ENV + echo "End database set up." - name: Run linting run: | flake8 --verbose From a69b43938830fcf8cc3e08f500f86b0c4156583f Mon Sep 17 00:00:00 2001 From: sea gold Date: Tue, 1 Jul 2025 23:37:54 +0200 Subject: [PATCH 08/12] check modified git action again the tests --- .github/workflows/github-action.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index d14582f..be915ad 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -45,15 +45,15 @@ jobs: - name: Set environment variables for testing run: | echo "Start Postgresql database set up" - echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV - echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV - echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV - echo "DB_PORT=${{ secrets.DB_PORT }}" >> $GITHUB_ENV - echo "TEST_DB_NAME=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV - echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV - echo "ALGORITHM=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV - echo "ACCESS_TOKEN_EXPIRE_MINUTES=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV - echo "ALEMBIC_DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOSTNAME}:${DB_PORT}/${TEST_DB_NAME}" >> $GITHUB_ENV + echo "db_username=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV + echo "db_password=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV + echo "db_hostname=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV + echo "db_port=${{ secrets.DB_PORT }}" >> $GITHUB_ENV + echo "test_db_name=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV + echo "secret_key=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV + echo "algorithm=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV + echo "access_token_expire_minutes=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV + echo "alembic_database_url=postgresql://${db_username}:${db_password}@${db_hostname}:${db_port}/${test_db_name}" echo "End database set up." - name: Run linting run: | From 592c6384458f72cf270398bc724490c41b9cdc21 Mon Sep 17 00:00:00 2001 From: sea gold Date: Wed, 2 Jul 2025 00:19:27 +0200 Subject: [PATCH 09/12] check modified git action again --- .github/workflows/github-action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index be915ad..d593795 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -53,7 +53,7 @@ jobs: echo "secret_key=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV echo "algorithm=${{ secrets.ALGORITHM }}" >> $GITHUB_ENV echo "access_token_expire_minutes=${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" >> $GITHUB_ENV - echo "alembic_database_url=postgresql://${db_username}:${db_password}@${db_hostname}:${db_port}/${test_db_name}" + echo "alembic_database_url=postgresql://${{ secrets.DB_USERNAME }}:${{ secrets.DB_PASSWORD }}@${{ secrets.DB_HOSTNAME }}:${{ secrets.DB_PORT }}/${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV echo "End database set up." - name: Run linting run: | From ba27d97a5e88fad99a6b4bce2d0985e136109505 Mon Sep 17 00:00:00 2001 From: sea gold Date: Wed, 2 Jul 2025 00:34:08 +0200 Subject: [PATCH 10/12] check modified git action --- settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index 80445d2..b929258 100644 --- a/settings.py +++ b/settings.py @@ -1,6 +1,6 @@ from pydantic_settings import BaseSettings from dotenv import load_dotenv - +from typing import Optional load_dotenv() @@ -14,7 +14,7 @@ class ConfigSettings(BaseSettings): algorithm: str access_token_expire_minutes: int test_db_name: str - alembic_database_url: str + alembic_database_url: Optional[str] = None class Config: env_file = ".env" From e88c084e005d15e3cd007833cc3349cd61e7f0ce Mon Sep 17 00:00:00 2001 From: sea gold Date: Wed, 2 Jul 2025 00:51:50 +0200 Subject: [PATCH 11/12] all checks for now --- .github/workflows/github-action.yaml | 1 + settings.py | 2 +- tests/conftest.py | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-action.yaml b/.github/workflows/github-action.yaml index d593795..5934b81 100644 --- a/.github/workflows/github-action.yaml +++ b/.github/workflows/github-action.yaml @@ -48,6 +48,7 @@ jobs: echo "db_username=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV echo "db_password=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV echo "db_hostname=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV + echo "db_name=${{ secrets.DB_NAME }}" >> $GITHUB_ENV echo "db_port=${{ secrets.DB_PORT }}" >> $GITHUB_ENV echo "test_db_name=${{ secrets.TEST_DB_NAME }}" >> $GITHUB_ENV echo "secret_key=${{ secrets.SECRET_KEY }}" >> $GITHUB_ENV diff --git a/settings.py b/settings.py index b929258..d91f7d9 100644 --- a/settings.py +++ b/settings.py @@ -20,4 +20,4 @@ class Config: env_file = ".env" -Setting = ConfigSettings() +Setting = ConfigSettings() \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index d9456ca..be14035 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,9 @@ @pytest.fixture def session(): alembic_cfg = Config("alembic.ini") + logger.info("Remove all tables from test database") command.downgrade(alembic_cfg, 'base') + logger.info("Apply all migrations for tests") command.upgrade(alembic_cfg, "head") db = TestSessionLocal() try: From 3bf3b0b4f6e4987fb51838b20cf50894728cb91f Mon Sep 17 00:00:00 2001 From: sea gold Date: Wed, 2 Jul 2025 00:53:28 +0200 Subject: [PATCH 12/12] fixed linting error again --- settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.py b/settings.py index d91f7d9..b929258 100644 --- a/settings.py +++ b/settings.py @@ -20,4 +20,4 @@ class Config: env_file = ".env" -Setting = ConfigSettings() \ No newline at end of file +Setting = ConfigSettings()