From 4c6bf55bcc4daff64b199063e182387e784295c6 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Sat, 28 Feb 2026 18:37:56 +0100 Subject: [PATCH] Refactor(backend): improve formatting in services, repositories & tests - Consistently format multi-line function headers, exception handling, and repository method calls for readability. - Reorganize misplaced imports across modules (e.g., services & tests) into proper sorted order. - Adjust indentation, line breaks, and spacing inconsistencies in tests and migration files. - Cleanup unnecessary trailing newlines and reorganize `__all__` declarations for consistency. --- backend/app/alembic/env.py | 1 + .../alembic/versions/0001_initial_models.py | 646 +++++++++++------- .../versions/0002_add_performance_indexes.py | 9 +- ...ount_token_fields_drop_encrypted_suffix.py | 17 +- backend/app/api/routes/admin.py | 6 +- backend/app/api/routes/auth.py | 4 +- backend/app/api/routes/oauth.py | 2 +- backend/app/api/routes/organizations.py | 2 +- backend/app/api/routes/sessions.py | 2 +- backend/app/api/routes/users.py | 6 +- backend/app/init_db.py | 2 +- backend/app/repositories/__init__.py | 22 +- backend/app/repositories/base.py | 1 - backend/app/repositories/oauth_account.py | 4 +- backend/app/repositories/oauth_client.py | 4 +- backend/app/repositories/oauth_consent.py | 7 +- .../app/repositories/oauth_provider_token.py | 8 +- backend/app/repositories/organization.py | 14 +- backend/app/repositories/session.py | 2 +- backend/app/repositories/user.py | 4 +- backend/app/services/__init__.py | 4 +- .../app/services/oauth_provider_service.py | 14 +- backend/app/services/oauth_service.py | 14 +- backend/app/services/organization_service.py | 4 +- backend/app/services/session_service.py | 8 +- backend/app/services/user_service.py | 8 +- backend/tests/api/test_auth_security.py | 2 +- .../tests/api/test_permissions_security.py | 2 +- backend/tests/api/test_users.py | 9 +- backend/tests/repositories/test_base.py | 31 +- .../repositories/test_base_db_failures.py | 8 +- backend/tests/repositories/test_oauth.py | 3 +- .../tests/repositories/test_organization.py | 9 +- backend/tests/repositories/test_session.py | 2 +- .../repositories/test_session_db_failures.py | 10 +- .../services/test_organization_service.py | 7 +- .../tests/services/test_session_service.py | 3 +- backend/tests/services/test_user_service.py | 3 +- 38 files changed, 567 insertions(+), 337 deletions(-) diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index 764852a..524f29a 100644 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -40,6 +40,7 @@ def include_object(object, name, type_, reflected, compare_to): return False return True + # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: diff --git a/backend/app/alembic/versions/0001_initial_models.py b/backend/app/alembic/versions/0001_initial_models.py index 86b7eda..ccfd14a 100644 --- a/backend/app/alembic/versions/0001_initial_models.py +++ b/backend/app/alembic/versions/0001_initial_models.py @@ -1,262 +1,446 @@ """initial models Revision ID: 0001 -Revises: +Revises: Create Date: 2025-11-27 09:08:09.464506 """ -from typing import Sequence, Union -from alembic import op +from collections.abc import Sequence + import sqlalchemy as sa +from alembic import op from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision: str = '0001' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +revision: str = "0001" +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('oauth_states', - sa.Column('state', sa.String(length=255), nullable=False), - sa.Column('code_verifier', sa.String(length=128), nullable=True), - sa.Column('nonce', sa.String(length=255), nullable=True), - sa.Column('provider', sa.String(length=50), nullable=False), - sa.Column('redirect_uri', sa.String(length=500), nullable=True), - sa.Column('user_id', sa.UUID(), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "oauth_states", + sa.Column("state", sa.String(length=255), nullable=False), + sa.Column("code_verifier", sa.String(length=128), nullable=True), + sa.Column("nonce", sa.String(length=255), nullable=True), + sa.Column("provider", sa.String(length=50), nullable=False), + sa.Column("redirect_uri", sa.String(length=500), nullable=True), + sa.Column("user_id", sa.UUID(), nullable=True), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_oauth_states_state'), 'oauth_states', ['state'], unique=True) - op.create_table('organizations', - sa.Column('name', sa.String(length=255), nullable=False), - sa.Column('slug', sa.String(length=255), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('settings', postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_oauth_states_state"), "oauth_states", ["state"], unique=True ) - op.create_index(op.f('ix_organizations_is_active'), 'organizations', ['is_active'], unique=False) - op.create_index(op.f('ix_organizations_name'), 'organizations', ['name'], unique=False) - op.create_index('ix_organizations_name_active', 'organizations', ['name', 'is_active'], unique=False) - op.create_index(op.f('ix_organizations_slug'), 'organizations', ['slug'], unique=True) - op.create_index('ix_organizations_slug_active', 'organizations', ['slug', 'is_active'], unique=False) - op.create_table('users', - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('password_hash', sa.String(length=255), nullable=True), - sa.Column('first_name', sa.String(length=100), nullable=False), - sa.Column('last_name', sa.String(length=100), nullable=True), - sa.Column('phone_number', sa.String(length=20), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('is_superuser', sa.Boolean(), nullable=False), - sa.Column('preferences', postgresql.JSONB(astext_type=sa.Text()), nullable=True), - sa.Column('locale', sa.String(length=10), nullable=True), - sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.PrimaryKeyConstraint('id') + op.create_table( + "organizations", + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("slug", sa.String(length=255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("settings", postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_users_deleted_at'), 'users', ['deleted_at'], unique=False) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_index(op.f('ix_users_is_active'), 'users', ['is_active'], unique=False) - op.create_index(op.f('ix_users_is_superuser'), 'users', ['is_superuser'], unique=False) - op.create_index(op.f('ix_users_locale'), 'users', ['locale'], unique=False) - op.create_table('oauth_accounts', - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('provider', sa.String(length=50), nullable=False), - sa.Column('provider_user_id', sa.String(length=255), nullable=False), - sa.Column('provider_email', sa.String(length=255), nullable=True), - sa.Column('access_token_encrypted', sa.String(length=2048), nullable=True), - sa.Column('refresh_token_encrypted', sa.String(length=2048), nullable=True), - sa.Column('token_expires_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('provider', 'provider_user_id', name='uq_oauth_provider_user') + op.create_index( + op.f("ix_organizations_is_active"), "organizations", ["is_active"], unique=False ) - op.create_index(op.f('ix_oauth_accounts_provider'), 'oauth_accounts', ['provider'], unique=False) - op.create_index(op.f('ix_oauth_accounts_provider_email'), 'oauth_accounts', ['provider_email'], unique=False) - op.create_index(op.f('ix_oauth_accounts_user_id'), 'oauth_accounts', ['user_id'], unique=False) - op.create_index('ix_oauth_accounts_user_provider', 'oauth_accounts', ['user_id', 'provider'], unique=False) - op.create_table('oauth_clients', - sa.Column('client_id', sa.String(length=64), nullable=False), - sa.Column('client_secret_hash', sa.String(length=255), nullable=True), - sa.Column('client_name', sa.String(length=255), nullable=False), - sa.Column('client_description', sa.String(length=1000), nullable=True), - sa.Column('client_type', sa.String(length=20), nullable=False), - sa.Column('redirect_uris', postgresql.JSONB(astext_type=sa.Text()), nullable=False), - sa.Column('allowed_scopes', postgresql.JSONB(astext_type=sa.Text()), nullable=False), - sa.Column('access_token_lifetime', sa.String(length=10), nullable=False), - sa.Column('refresh_token_lifetime', sa.String(length=10), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('owner_user_id', sa.UUID(), nullable=True), - sa.Column('mcp_server_url', sa.String(length=2048), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_organizations_name"), "organizations", ["name"], unique=False ) - op.create_index(op.f('ix_oauth_clients_client_id'), 'oauth_clients', ['client_id'], unique=True) - op.create_index(op.f('ix_oauth_clients_is_active'), 'oauth_clients', ['is_active'], unique=False) - op.create_table('user_organizations', - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('organization_id', sa.UUID(), nullable=False), - sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'MEMBER', 'GUEST', name='organizationrole'), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('custom_permissions', sa.String(length=500), nullable=True), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'organization_id') + op.create_index( + "ix_organizations_name_active", + "organizations", + ["name", "is_active"], + unique=False, ) - op.create_index('ix_user_org_org_active', 'user_organizations', ['organization_id', 'is_active'], unique=False) - op.create_index('ix_user_org_role', 'user_organizations', ['role'], unique=False) - op.create_index('ix_user_org_user_active', 'user_organizations', ['user_id', 'is_active'], unique=False) - op.create_index(op.f('ix_user_organizations_is_active'), 'user_organizations', ['is_active'], unique=False) - op.create_table('user_sessions', - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('refresh_token_jti', sa.String(length=255), nullable=False), - sa.Column('device_name', sa.String(length=255), nullable=True), - sa.Column('device_id', sa.String(length=255), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('user_agent', sa.String(length=500), nullable=True), - sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('location_city', sa.String(length=100), nullable=True), - sa.Column('location_country', sa.String(length=100), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_organizations_slug"), "organizations", ["slug"], unique=True ) - op.create_index(op.f('ix_user_sessions_is_active'), 'user_sessions', ['is_active'], unique=False) - op.create_index('ix_user_sessions_jti_active', 'user_sessions', ['refresh_token_jti', 'is_active'], unique=False) - op.create_index(op.f('ix_user_sessions_refresh_token_jti'), 'user_sessions', ['refresh_token_jti'], unique=True) - op.create_index('ix_user_sessions_user_active', 'user_sessions', ['user_id', 'is_active'], unique=False) - op.create_index(op.f('ix_user_sessions_user_id'), 'user_sessions', ['user_id'], unique=False) - op.create_table('oauth_authorization_codes', - sa.Column('code', sa.String(length=128), nullable=False), - sa.Column('client_id', sa.String(length=64), nullable=False), - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('redirect_uri', sa.String(length=2048), nullable=False), - sa.Column('scope', sa.String(length=1000), nullable=False), - sa.Column('code_challenge', sa.String(length=128), nullable=True), - sa.Column('code_challenge_method', sa.String(length=10), nullable=True), - sa.Column('state', sa.String(length=256), nullable=True), - sa.Column('nonce', sa.String(length=256), nullable=True), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('used', sa.Boolean(), nullable=False), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index( + "ix_organizations_slug_active", + "organizations", + ["slug", "is_active"], + unique=False, ) - op.create_index('ix_oauth_authorization_codes_client_user', 'oauth_authorization_codes', ['client_id', 'user_id'], unique=False) - op.create_index(op.f('ix_oauth_authorization_codes_code'), 'oauth_authorization_codes', ['code'], unique=True) - op.create_index('ix_oauth_authorization_codes_expires_at', 'oauth_authorization_codes', ['expires_at'], unique=False) - op.create_table('oauth_consents', - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('client_id', sa.String(length=64), nullable=False), - sa.Column('granted_scopes', sa.String(length=1000), nullable=False), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "users", + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("password_hash", sa.String(length=255), nullable=True), + sa.Column("first_name", sa.String(length=100), nullable=False), + sa.Column("last_name", sa.String(length=100), nullable=True), + sa.Column("phone_number", sa.String(length=20), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("is_superuser", sa.Boolean(), nullable=False), + sa.Column( + "preferences", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + sa.Column("locale", sa.String(length=10), nullable=True), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), ) - op.create_index('ix_oauth_consents_user_client', 'oauth_consents', ['user_id', 'client_id'], unique=True) - op.create_table('oauth_provider_refresh_tokens', - sa.Column('token_hash', sa.String(length=64), nullable=False), - sa.Column('jti', sa.String(length=64), nullable=False), - sa.Column('client_id', sa.String(length=64), nullable=False), - sa.Column('user_id', sa.UUID(), nullable=False), - sa.Column('scope', sa.String(length=1000), nullable=False), - sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('revoked', sa.Boolean(), nullable=False), - sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True), - sa.Column('device_info', sa.String(length=500), nullable=True), - sa.Column('ip_address', sa.String(length=45), nullable=True), - sa.Column('id', sa.UUID(), nullable=False), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), - sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_users_deleted_at"), "users", ["deleted_at"], unique=False) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + op.create_index(op.f("ix_users_is_active"), "users", ["is_active"], unique=False) + op.create_index( + op.f("ix_users_is_superuser"), "users", ["is_superuser"], unique=False + ) + op.create_index(op.f("ix_users_locale"), "users", ["locale"], unique=False) + op.create_table( + "oauth_accounts", + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("provider", sa.String(length=50), nullable=False), + sa.Column("provider_user_id", sa.String(length=255), nullable=False), + sa.Column("provider_email", sa.String(length=255), nullable=True), + sa.Column("access_token_encrypted", sa.String(length=2048), nullable=True), + sa.Column("refresh_token_encrypted", sa.String(length=2048), nullable=True), + sa.Column("token_expires_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "provider", "provider_user_id", name="uq_oauth_provider_user" + ), + ) + op.create_index( + op.f("ix_oauth_accounts_provider"), "oauth_accounts", ["provider"], unique=False + ) + op.create_index( + op.f("ix_oauth_accounts_provider_email"), + "oauth_accounts", + ["provider_email"], + unique=False, + ) + op.create_index( + op.f("ix_oauth_accounts_user_id"), "oauth_accounts", ["user_id"], unique=False + ) + op.create_index( + "ix_oauth_accounts_user_provider", + "oauth_accounts", + ["user_id", "provider"], + unique=False, + ) + op.create_table( + "oauth_clients", + sa.Column("client_id", sa.String(length=64), nullable=False), + sa.Column("client_secret_hash", sa.String(length=255), nullable=True), + sa.Column("client_name", sa.String(length=255), nullable=False), + sa.Column("client_description", sa.String(length=1000), nullable=True), + sa.Column("client_type", sa.String(length=20), nullable=False), + sa.Column( + "redirect_uris", postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column( + "allowed_scopes", postgresql.JSONB(astext_type=sa.Text()), nullable=False + ), + sa.Column("access_token_lifetime", sa.String(length=10), nullable=False), + sa.Column("refresh_token_lifetime", sa.String(length=10), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("owner_user_id", sa.UUID(), nullable=True), + sa.Column("mcp_server_url", sa.String(length=2048), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(["owner_user_id"], ["users.id"], ondelete="SET NULL"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_oauth_clients_client_id"), "oauth_clients", ["client_id"], unique=True + ) + op.create_index( + op.f("ix_oauth_clients_is_active"), "oauth_clients", ["is_active"], unique=False + ) + op.create_table( + "user_organizations", + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("organization_id", sa.UUID(), nullable=False), + sa.Column( + "role", + sa.Enum("OWNER", "ADMIN", "MEMBER", "GUEST", name="organizationrole"), + nullable=False, + ), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("custom_permissions", sa.String(length=500), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["organization_id"], ["organizations.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("user_id", "organization_id"), + ) + op.create_index( + "ix_user_org_org_active", + "user_organizations", + ["organization_id", "is_active"], + unique=False, + ) + op.create_index("ix_user_org_role", "user_organizations", ["role"], unique=False) + op.create_index( + "ix_user_org_user_active", + "user_organizations", + ["user_id", "is_active"], + unique=False, + ) + op.create_index( + op.f("ix_user_organizations_is_active"), + "user_organizations", + ["is_active"], + unique=False, + ) + op.create_table( + "user_sessions", + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("refresh_token_jti", sa.String(length=255), nullable=False), + sa.Column("device_name", sa.String(length=255), nullable=True), + sa.Column("device_id", sa.String(length=255), nullable=True), + sa.Column("ip_address", sa.String(length=45), nullable=True), + sa.Column("user_agent", sa.String(length=500), nullable=True), + sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("location_city", sa.String(length=100), nullable=True), + sa.Column("location_country", sa.String(length=100), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_user_sessions_is_active"), "user_sessions", ["is_active"], unique=False + ) + op.create_index( + "ix_user_sessions_jti_active", + "user_sessions", + ["refresh_token_jti", "is_active"], + unique=False, + ) + op.create_index( + op.f("ix_user_sessions_refresh_token_jti"), + "user_sessions", + ["refresh_token_jti"], + unique=True, + ) + op.create_index( + "ix_user_sessions_user_active", + "user_sessions", + ["user_id", "is_active"], + unique=False, + ) + op.create_index( + op.f("ix_user_sessions_user_id"), "user_sessions", ["user_id"], unique=False + ) + op.create_table( + "oauth_authorization_codes", + sa.Column("code", sa.String(length=128), nullable=False), + sa.Column("client_id", sa.String(length=64), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("redirect_uri", sa.String(length=2048), nullable=False), + sa.Column("scope", sa.String(length=1000), nullable=False), + sa.Column("code_challenge", sa.String(length=128), nullable=True), + sa.Column("code_challenge_method", sa.String(length=10), nullable=True), + sa.Column("state", sa.String(length=256), nullable=True), + sa.Column("nonce", sa.String(length=256), nullable=True), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("used", sa.Boolean(), nullable=False), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["client_id"], ["oauth_clients.client_id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_oauth_authorization_codes_client_user", + "oauth_authorization_codes", + ["client_id", "user_id"], + unique=False, + ) + op.create_index( + op.f("ix_oauth_authorization_codes_code"), + "oauth_authorization_codes", + ["code"], + unique=True, + ) + op.create_index( + "ix_oauth_authorization_codes_expires_at", + "oauth_authorization_codes", + ["expires_at"], + unique=False, + ) + op.create_table( + "oauth_consents", + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("client_id", sa.String(length=64), nullable=False), + sa.Column("granted_scopes", sa.String(length=1000), nullable=False), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["client_id"], ["oauth_clients.client_id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_oauth_consents_user_client", + "oauth_consents", + ["user_id", "client_id"], + unique=True, + ) + op.create_table( + "oauth_provider_refresh_tokens", + sa.Column("token_hash", sa.String(length=64), nullable=False), + sa.Column("jti", sa.String(length=64), nullable=False), + sa.Column("client_id", sa.String(length=64), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("scope", sa.String(length=1000), nullable=False), + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("revoked", sa.Boolean(), nullable=False), + sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("device_info", sa.String(length=500), nullable=True), + sa.Column("ip_address", sa.String(length=45), nullable=True), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["client_id"], ["oauth_clients.client_id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + "ix_oauth_provider_refresh_tokens_client_user", + "oauth_provider_refresh_tokens", + ["client_id", "user_id"], + unique=False, + ) + op.create_index( + "ix_oauth_provider_refresh_tokens_expires_at", + "oauth_provider_refresh_tokens", + ["expires_at"], + unique=False, + ) + op.create_index( + op.f("ix_oauth_provider_refresh_tokens_jti"), + "oauth_provider_refresh_tokens", + ["jti"], + unique=True, + ) + op.create_index( + op.f("ix_oauth_provider_refresh_tokens_revoked"), + "oauth_provider_refresh_tokens", + ["revoked"], + unique=False, + ) + op.create_index( + op.f("ix_oauth_provider_refresh_tokens_token_hash"), + "oauth_provider_refresh_tokens", + ["token_hash"], + unique=True, + ) + op.create_index( + "ix_oauth_provider_refresh_tokens_user_revoked", + "oauth_provider_refresh_tokens", + ["user_id", "revoked"], + unique=False, ) - op.create_index('ix_oauth_provider_refresh_tokens_client_user', 'oauth_provider_refresh_tokens', ['client_id', 'user_id'], unique=False) - op.create_index('ix_oauth_provider_refresh_tokens_expires_at', 'oauth_provider_refresh_tokens', ['expires_at'], unique=False) - op.create_index(op.f('ix_oauth_provider_refresh_tokens_jti'), 'oauth_provider_refresh_tokens', ['jti'], unique=True) - op.create_index(op.f('ix_oauth_provider_refresh_tokens_revoked'), 'oauth_provider_refresh_tokens', ['revoked'], unique=False) - op.create_index(op.f('ix_oauth_provider_refresh_tokens_token_hash'), 'oauth_provider_refresh_tokens', ['token_hash'], unique=True) - op.create_index('ix_oauth_provider_refresh_tokens_user_revoked', 'oauth_provider_refresh_tokens', ['user_id', 'revoked'], unique=False) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index('ix_oauth_provider_refresh_tokens_user_revoked', table_name='oauth_provider_refresh_tokens') - op.drop_index(op.f('ix_oauth_provider_refresh_tokens_token_hash'), table_name='oauth_provider_refresh_tokens') - op.drop_index(op.f('ix_oauth_provider_refresh_tokens_revoked'), table_name='oauth_provider_refresh_tokens') - op.drop_index(op.f('ix_oauth_provider_refresh_tokens_jti'), table_name='oauth_provider_refresh_tokens') - op.drop_index('ix_oauth_provider_refresh_tokens_expires_at', table_name='oauth_provider_refresh_tokens') - op.drop_index('ix_oauth_provider_refresh_tokens_client_user', table_name='oauth_provider_refresh_tokens') - op.drop_table('oauth_provider_refresh_tokens') - op.drop_index('ix_oauth_consents_user_client', table_name='oauth_consents') - op.drop_table('oauth_consents') - op.drop_index('ix_oauth_authorization_codes_expires_at', table_name='oauth_authorization_codes') - op.drop_index(op.f('ix_oauth_authorization_codes_code'), table_name='oauth_authorization_codes') - op.drop_index('ix_oauth_authorization_codes_client_user', table_name='oauth_authorization_codes') - op.drop_table('oauth_authorization_codes') - op.drop_index(op.f('ix_user_sessions_user_id'), table_name='user_sessions') - op.drop_index('ix_user_sessions_user_active', table_name='user_sessions') - op.drop_index(op.f('ix_user_sessions_refresh_token_jti'), table_name='user_sessions') - op.drop_index('ix_user_sessions_jti_active', table_name='user_sessions') - op.drop_index(op.f('ix_user_sessions_is_active'), table_name='user_sessions') - op.drop_table('user_sessions') - op.drop_index(op.f('ix_user_organizations_is_active'), table_name='user_organizations') - op.drop_index('ix_user_org_user_active', table_name='user_organizations') - op.drop_index('ix_user_org_role', table_name='user_organizations') - op.drop_index('ix_user_org_org_active', table_name='user_organizations') - op.drop_table('user_organizations') - op.drop_index(op.f('ix_oauth_clients_is_active'), table_name='oauth_clients') - op.drop_index(op.f('ix_oauth_clients_client_id'), table_name='oauth_clients') - op.drop_table('oauth_clients') - op.drop_index('ix_oauth_accounts_user_provider', table_name='oauth_accounts') - op.drop_index(op.f('ix_oauth_accounts_user_id'), table_name='oauth_accounts') - op.drop_index(op.f('ix_oauth_accounts_provider_email'), table_name='oauth_accounts') - op.drop_index(op.f('ix_oauth_accounts_provider'), table_name='oauth_accounts') - op.drop_table('oauth_accounts') - op.drop_index(op.f('ix_users_locale'), table_name='users') - op.drop_index(op.f('ix_users_is_superuser'), table_name='users') - op.drop_index(op.f('ix_users_is_active'), table_name='users') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_index(op.f('ix_users_deleted_at'), table_name='users') - op.drop_table('users') - op.drop_index('ix_organizations_slug_active', table_name='organizations') - op.drop_index(op.f('ix_organizations_slug'), table_name='organizations') - op.drop_index('ix_organizations_name_active', table_name='organizations') - op.drop_index(op.f('ix_organizations_name'), table_name='organizations') - op.drop_index(op.f('ix_organizations_is_active'), table_name='organizations') - op.drop_table('organizations') - op.drop_index(op.f('ix_oauth_states_state'), table_name='oauth_states') - op.drop_table('oauth_states') + op.drop_index( + "ix_oauth_provider_refresh_tokens_user_revoked", + table_name="oauth_provider_refresh_tokens", + ) + op.drop_index( + op.f("ix_oauth_provider_refresh_tokens_token_hash"), + table_name="oauth_provider_refresh_tokens", + ) + op.drop_index( + op.f("ix_oauth_provider_refresh_tokens_revoked"), + table_name="oauth_provider_refresh_tokens", + ) + op.drop_index( + op.f("ix_oauth_provider_refresh_tokens_jti"), + table_name="oauth_provider_refresh_tokens", + ) + op.drop_index( + "ix_oauth_provider_refresh_tokens_expires_at", + table_name="oauth_provider_refresh_tokens", + ) + op.drop_index( + "ix_oauth_provider_refresh_tokens_client_user", + table_name="oauth_provider_refresh_tokens", + ) + op.drop_table("oauth_provider_refresh_tokens") + op.drop_index("ix_oauth_consents_user_client", table_name="oauth_consents") + op.drop_table("oauth_consents") + op.drop_index( + "ix_oauth_authorization_codes_expires_at", + table_name="oauth_authorization_codes", + ) + op.drop_index( + op.f("ix_oauth_authorization_codes_code"), + table_name="oauth_authorization_codes", + ) + op.drop_index( + "ix_oauth_authorization_codes_client_user", + table_name="oauth_authorization_codes", + ) + op.drop_table("oauth_authorization_codes") + op.drop_index(op.f("ix_user_sessions_user_id"), table_name="user_sessions") + op.drop_index("ix_user_sessions_user_active", table_name="user_sessions") + op.drop_index( + op.f("ix_user_sessions_refresh_token_jti"), table_name="user_sessions" + ) + op.drop_index("ix_user_sessions_jti_active", table_name="user_sessions") + op.drop_index(op.f("ix_user_sessions_is_active"), table_name="user_sessions") + op.drop_table("user_sessions") + op.drop_index( + op.f("ix_user_organizations_is_active"), table_name="user_organizations" + ) + op.drop_index("ix_user_org_user_active", table_name="user_organizations") + op.drop_index("ix_user_org_role", table_name="user_organizations") + op.drop_index("ix_user_org_org_active", table_name="user_organizations") + op.drop_table("user_organizations") + op.drop_index(op.f("ix_oauth_clients_is_active"), table_name="oauth_clients") + op.drop_index(op.f("ix_oauth_clients_client_id"), table_name="oauth_clients") + op.drop_table("oauth_clients") + op.drop_index("ix_oauth_accounts_user_provider", table_name="oauth_accounts") + op.drop_index(op.f("ix_oauth_accounts_user_id"), table_name="oauth_accounts") + op.drop_index(op.f("ix_oauth_accounts_provider_email"), table_name="oauth_accounts") + op.drop_index(op.f("ix_oauth_accounts_provider"), table_name="oauth_accounts") + op.drop_table("oauth_accounts") + op.drop_index(op.f("ix_users_locale"), table_name="users") + op.drop_index(op.f("ix_users_is_superuser"), table_name="users") + op.drop_index(op.f("ix_users_is_active"), table_name="users") + op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_index(op.f("ix_users_deleted_at"), table_name="users") + op.drop_table("users") + op.drop_index("ix_organizations_slug_active", table_name="organizations") + op.drop_index(op.f("ix_organizations_slug"), table_name="organizations") + op.drop_index("ix_organizations_name_active", table_name="organizations") + op.drop_index(op.f("ix_organizations_name"), table_name="organizations") + op.drop_index(op.f("ix_organizations_is_active"), table_name="organizations") + op.drop_table("organizations") + op.drop_index(op.f("ix_oauth_states_state"), table_name="oauth_states") + op.drop_table("oauth_states") # ### end Alembic commands ### diff --git a/backend/app/alembic/versions/0002_add_performance_indexes.py b/backend/app/alembic/versions/0002_add_performance_indexes.py index 0754cc5..1c7987f 100644 --- a/backend/app/alembic/versions/0002_add_performance_indexes.py +++ b/backend/app/alembic/versions/0002_add_performance_indexes.py @@ -114,8 +114,13 @@ def upgrade() -> None: def downgrade() -> None: # Drop indexes in reverse order - op.drop_index("ix_perf_oauth_auth_codes_expires", table_name="oauth_authorization_codes") - op.drop_index("ix_perf_oauth_refresh_tokens_expires", table_name="oauth_provider_refresh_tokens") + op.drop_index( + "ix_perf_oauth_auth_codes_expires", table_name="oauth_authorization_codes" + ) + op.drop_index( + "ix_perf_oauth_refresh_tokens_expires", + table_name="oauth_provider_refresh_tokens", + ) op.drop_index("ix_perf_user_sessions_expires", table_name="user_sessions") op.drop_index("ix_perf_organizations_slug_lower", table_name="organizations") op.drop_index("ix_perf_users_active", table_name="users") diff --git a/backend/app/alembic/versions/0003_rename_oauth_account_token_fields_drop_encrypted_suffix.py b/backend/app/alembic/versions/0003_rename_oauth_account_token_fields_drop_encrypted_suffix.py index b717976..925ca2c 100644 --- a/backend/app/alembic/versions/0003_rename_oauth_account_token_fields_drop_encrypted_suffix.py +++ b/backend/app/alembic/versions/0003_rename_oauth_account_token_fields_drop_encrypted_suffix.py @@ -8,7 +8,6 @@ Create Date: 2026-02-27 01:03:18.869178 from collections.abc import Sequence -import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. @@ -19,10 +18,18 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: - op.alter_column("oauth_accounts", "access_token_encrypted", new_column_name="access_token") - op.alter_column("oauth_accounts", "refresh_token_encrypted", new_column_name="refresh_token") + op.alter_column( + "oauth_accounts", "access_token_encrypted", new_column_name="access_token" + ) + op.alter_column( + "oauth_accounts", "refresh_token_encrypted", new_column_name="refresh_token" + ) def downgrade() -> None: - op.alter_column("oauth_accounts", "access_token", new_column_name="access_token_encrypted") - op.alter_column("oauth_accounts", "refresh_token", new_column_name="refresh_token_encrypted") + op.alter_column( + "oauth_accounts", "access_token", new_column_name="access_token_encrypted" + ) + op.alter_column( + "oauth_accounts", "refresh_token", new_column_name="refresh_token_encrypted" + ) diff --git a/backend/app/api/routes/admin.py b/backend/app/api/routes/admin.py index f2f2bb8..f1cfad1 100755 --- a/backend/app/api/routes/admin.py +++ b/backend/app/api/routes/admin.py @@ -27,9 +27,6 @@ from app.core.exceptions import ( from app.core.repository_exceptions import DuplicateEntryError from app.models.user import User from app.models.user_organization import OrganizationRole -from app.services.organization_service import organization_service -from app.services.session_service import session_service -from app.services.user_service import user_service from app.schemas.common import ( MessageResponse, PaginatedResponse, @@ -45,6 +42,9 @@ from app.schemas.organizations import ( ) from app.schemas.sessions import AdminSessionResponse from app.schemas.users import UserCreate, UserResponse, UserUpdate +from app.services.organization_service import organization_service +from app.services.session_service import session_service +from app.services.user_service import user_service logger = logging.getLogger(__name__) diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py index 0160153..a884d66 100755 --- a/backend/app/api/routes/auth.py +++ b/backend/app/api/routes/auth.py @@ -429,9 +429,7 @@ async def confirm_password_reset( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=err_msg ) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail=err_msg - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=err_msg) # SECURITY: Invalidate all existing sessions after password reset # This prevents stolen sessions from being used after password change diff --git a/backend/app/api/routes/oauth.py b/backend/app/api/routes/oauth.py index c5ab491..eb28c07 100644 --- a/backend/app/api/routes/oauth.py +++ b/backend/app/api/routes/oauth.py @@ -25,7 +25,6 @@ from app.core.auth import decode_token from app.core.config import settings from app.core.database import get_db from app.core.exceptions import AuthenticationError as AuthError -from app.services.session_service import session_service from app.models.user import User from app.schemas.oauth import ( OAuthAccountsListResponse, @@ -37,6 +36,7 @@ from app.schemas.oauth import ( from app.schemas.sessions import SessionCreate from app.schemas.users import Token from app.services.oauth_service import OAuthService +from app.services.session_service import session_service from app.utils.device import extract_device_info router = APIRouter() diff --git a/backend/app/api/routes/organizations.py b/backend/app/api/routes/organizations.py index 8784987..90c7411 100755 --- a/backend/app/api/routes/organizations.py +++ b/backend/app/api/routes/organizations.py @@ -16,7 +16,6 @@ from app.api.dependencies.auth import get_current_user from app.api.dependencies.permissions import require_org_admin, require_org_membership from app.core.database import get_db from app.models.user import User -from app.services.organization_service import organization_service from app.schemas.common import ( PaginatedResponse, PaginationParams, @@ -27,6 +26,7 @@ from app.schemas.organizations import ( OrganizationResponse, OrganizationUpdate, ) +from app.services.organization_service import organization_service logger = logging.getLogger(__name__) diff --git a/backend/app/api/routes/sessions.py b/backend/app/api/routes/sessions.py index ebb7274..b17270f 100755 --- a/backend/app/api/routes/sessions.py +++ b/backend/app/api/routes/sessions.py @@ -18,9 +18,9 @@ from app.core.auth import decode_token from app.core.database import get_db from app.core.exceptions import AuthorizationError, ErrorCode, NotFoundError from app.models.user import User -from app.services.session_service import session_service from app.schemas.common import MessageResponse from app.schemas.sessions import SessionListResponse, SessionResponse +from app.services.session_service import session_service router = APIRouter() logger = logging.getLogger(__name__) diff --git a/backend/app/api/routes/users.py b/backend/app/api/routes/users.py index c6ff0a9..694b18b 100755 --- a/backend/app/api/routes/users.py +++ b/backend/app/api/routes/users.py @@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api.dependencies.auth import get_current_superuser, get_current_user from app.core.database import get_db -from app.core.exceptions import AuthorizationError, ErrorCode, NotFoundError +from app.core.exceptions import AuthorizationError, ErrorCode from app.models.user import User from app.schemas.common import ( MessageResponse, @@ -107,7 +107,9 @@ async def list_users( """, operation_id="get_current_user_profile", ) -async def get_current_user_profile(current_user: User = Depends(get_current_user)) -> Any: +async def get_current_user_profile( + current_user: User = Depends(get_current_user), +) -> Any: """Get current user's profile.""" return current_user diff --git a/backend/app/init_db.py b/backend/app/init_db.py index 07db637..ae79f6a 100644 --- a/backend/app/init_db.py +++ b/backend/app/init_db.py @@ -16,10 +16,10 @@ from sqlalchemy import select, text from app.core.config import settings from app.core.database import SessionLocal, engine -from app.repositories.user import user_repo as user_crud from app.models.organization import Organization from app.models.user import User from app.models.user_organization import UserOrganization +from app.repositories.user import user_repo as user_crud from app.schemas.users import UserCreate logger = logging.getLogger(__name__) diff --git a/backend/app/repositories/__init__.py b/backend/app/repositories/__init__.py index 0c62864..849fcde 100644 --- a/backend/app/repositories/__init__.py +++ b/backend/app/repositories/__init__.py @@ -18,22 +18,22 @@ from app.repositories.session import SessionRepository, session_repo from app.repositories.user import UserRepository, user_repo __all__ = [ - "UserRepository", - "user_repo", - "OrganizationRepository", - "organization_repo", - "SessionRepository", - "session_repo", "OAuthAccountRepository", - "oauth_account_repo", "OAuthAuthorizationCodeRepository", - "oauth_authorization_code_repo", "OAuthClientRepository", - "oauth_client_repo", "OAuthConsentRepository", - "oauth_consent_repo", "OAuthProviderTokenRepository", - "oauth_provider_token_repo", "OAuthStateRepository", + "OrganizationRepository", + "SessionRepository", + "UserRepository", + "oauth_account_repo", + "oauth_authorization_code_repo", + "oauth_client_repo", + "oauth_consent_repo", + "oauth_provider_token_repo", "oauth_state_repo", + "organization_repo", + "session_repo", + "user_repo", ] diff --git a/backend/app/repositories/base.py b/backend/app/repositories/base.py index d8cac40..09bb166 100644 --- a/backend/app/repositories/base.py +++ b/backend/app/repositories/base.py @@ -411,4 +411,3 @@ class BaseRepository[ exc_info=True, ) raise - diff --git a/backend/app/repositories/oauth_account.py b/backend/app/repositories/oauth_account.py index d24ac33..0cc4683 100644 --- a/backend/app/repositories/oauth_account.py +++ b/backend/app/repositories/oauth_account.py @@ -23,7 +23,9 @@ class EmptySchema(BaseModel): """Placeholder schema for repository operations that don't need update schemas.""" -class OAuthAccountRepository(BaseRepository[OAuthAccount, OAuthAccountCreate, EmptySchema]): +class OAuthAccountRepository( + BaseRepository[OAuthAccount, OAuthAccountCreate, EmptySchema] +): """Repository for OAuth account links.""" async def get_by_provider_id( diff --git a/backend/app/repositories/oauth_client.py b/backend/app/repositories/oauth_client.py index 445b93c..f38858b 100644 --- a/backend/app/repositories/oauth_client.py +++ b/backend/app/repositories/oauth_client.py @@ -22,7 +22,9 @@ class EmptySchema(BaseModel): """Placeholder schema for repository operations that don't need update schemas.""" -class OAuthClientRepository(BaseRepository[OAuthClient, OAuthClientCreate, EmptySchema]): +class OAuthClientRepository( + BaseRepository[OAuthClient, OAuthClientCreate, EmptySchema] +): """Repository for OAuth clients (provider mode).""" async def get_by_client_id( diff --git a/backend/app/repositories/oauth_consent.py b/backend/app/repositories/oauth_consent.py index 4aac4d8..4644f69 100644 --- a/backend/app/repositories/oauth_consent.py +++ b/backend/app/repositories/oauth_consent.py @@ -2,9 +2,8 @@ """Repository for OAuthConsent model.""" import logging -from uuid import UUID - from typing import Any +from uuid import UUID from sqlalchemy import and_, delete, select from sqlalchemy.ext.asyncio import AsyncSession @@ -49,7 +48,9 @@ class OAuthConsentRepository: consent = await self.get_consent(db, user_id=user_id, client_id=client_id) if consent: - existing = set(consent.granted_scopes.split()) if consent.granted_scopes else set() + existing = ( + set(consent.granted_scopes.split()) if consent.granted_scopes else set() + ) merged = existing | set(scopes) consent.granted_scopes = " ".join(sorted(merged)) # type: ignore[assignment] else: diff --git a/backend/app/repositories/oauth_provider_token.py b/backend/app/repositories/oauth_provider_token.py index 9696116..f547b22 100644 --- a/backend/app/repositories/oauth_provider_token.py +++ b/backend/app/repositories/oauth_provider_token.py @@ -99,9 +99,7 @@ class OAuthProviderTokenRepository: await db.commit() return count - async def revoke_all_for_user( - self, db: AsyncSession, *, user_id: UUID - ) -> int: + async def revoke_all_for_user(self, db: AsyncSession, *, user_id: UUID) -> int: """ Revoke all active tokens for a user across all clients. @@ -123,9 +121,7 @@ class OAuthProviderTokenRepository: await db.commit() return count - async def cleanup_expired( - self, db: AsyncSession, *, cutoff_days: int = 7 - ) -> int: + async def cleanup_expired(self, db: AsyncSession, *, cutoff_days: int = 7) -> int: """ Delete expired refresh tokens older than cutoff_days. diff --git a/backend/app/repositories/organization.py b/backend/app/repositories/organization.py index 11e93ad..fa3f002 100644 --- a/backend/app/repositories/organization.py +++ b/backend/app/repositories/organization.py @@ -22,7 +22,9 @@ from app.schemas.organizations import ( logger = logging.getLogger(__name__) -class OrganizationRepository(BaseRepository[Organization, OrganizationCreate, OrganizationUpdate]): +class OrganizationRepository( + BaseRepository[Organization, OrganizationCreate, OrganizationUpdate] +): """Repository for Organization model.""" async def get_by_slug(self, db: AsyncSession, *, slug: str) -> Organization | None: @@ -55,7 +57,11 @@ class OrganizationRepository(BaseRepository[Organization, OrganizationCreate, Or except IntegrityError as e: await db.rollback() error_msg = str(e.orig) if hasattr(e, "orig") else str(e) - if "slug" in error_msg.lower() or "unique" in error_msg.lower() or "duplicate" in error_msg.lower(): + if ( + "slug" in error_msg.lower() + or "unique" in error_msg.lower() + or "duplicate" in error_msg.lower() + ): logger.warning(f"Duplicate slug attempted: {obj_in.slug}") raise DuplicateEntryError( f"Organization with slug '{obj_in.slug}' already exists" @@ -235,7 +241,9 @@ class OrganizationRepository(BaseRepository[Organization, OrganizationCreate, Or await db.refresh(existing) return existing else: - raise DuplicateEntryError("User is already a member of this organization") + raise DuplicateEntryError( + "User is already a member of this organization" + ) user_org = UserOrganization( user_id=user_id, diff --git a/backend/app/repositories/session.py b/backend/app/repositories/session.py index 78a9953..3a22bfb 100644 --- a/backend/app/repositories/session.py +++ b/backend/app/repositories/session.py @@ -10,7 +10,7 @@ from sqlalchemy import and_, delete, func, select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from app.core.repository_exceptions import InvalidInputError, IntegrityConstraintError +from app.core.repository_exceptions import IntegrityConstraintError, InvalidInputError from app.models.user_session import UserSession from app.repositories.base import BaseRepository from app.schemas.sessions import SessionCreate, SessionUpdate diff --git a/backend/app/repositories/user.py b/backend/app/repositories/user.py index 97b4dcd..d4877f3 100644 --- a/backend/app/repositories/user.py +++ b/backend/app/repositories/user.py @@ -58,7 +58,9 @@ class UserRepository(BaseRepository[User, UserCreate, UserUpdate]): error_msg = str(e.orig) if hasattr(e, "orig") else str(e) if "email" in error_msg.lower(): logger.warning(f"Duplicate email attempted: {obj_in.email}") - raise DuplicateEntryError(f"User with email {obj_in.email} already exists") + raise DuplicateEntryError( + f"User with email {obj_in.email} already exists" + ) logger.error(f"Integrity error creating user: {error_msg}") raise DuplicateEntryError(f"Database integrity error: {error_msg}") except Exception as e: diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py index f06b9a5..e42abc9 100644 --- a/backend/app/services/__init__.py +++ b/backend/app/services/__init__.py @@ -9,11 +9,11 @@ from .user_service import UserService, user_service __all__ = [ "AuthService", "OAuthService", - "UserService", "OrganizationService", "SessionService", + "UserService", "oauth_provider_service", - "user_service", "organization_service", "session_service", + "user_service", ] diff --git a/backend/app/services/oauth_provider_service.py b/backend/app/services/oauth_provider_service.py index 0855fcb..03ccce2 100755 --- a/backend/app/services/oauth_provider_service.py +++ b/backend/app/services/oauth_provider_service.py @@ -30,13 +30,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.models.oauth_client import OAuthClient -from app.schemas.oauth import OAuthClientCreate from app.models.user import User from app.repositories.oauth_authorization_code import oauth_authorization_code_repo from app.repositories.oauth_client import oauth_client_repo from app.repositories.oauth_consent import oauth_consent_repo from app.repositories.oauth_provider_token import oauth_provider_token_repo from app.repositories.user import user_repo +from app.schemas.oauth import OAuthClientCreate logger = logging.getLogger(__name__) @@ -691,9 +691,7 @@ async def revoke_token( jti = payload.get("jti") if jti: # Find and revoke the associated refresh token - refresh_record = await oauth_provider_token_repo.get_by_jti( - db, jti=jti - ) + refresh_record = await oauth_provider_token_repo.get_by_jti(db, jti=jti) if refresh_record: if client_id and refresh_record.client_id != client_id: raise InvalidClientError("Token was not issued to this client") @@ -807,9 +805,7 @@ async def introspect_token( # Check if associated refresh token is revoked jti = payload.get("jti") if jti: - refresh_record = await oauth_provider_token_repo.get_by_jti( - db, jti=jti - ) + refresh_record = await oauth_provider_token_repo.get_by_jti(db, jti=jti) if refresh_record and refresh_record.revoked: return {"active": False} @@ -862,7 +858,9 @@ async def get_consent( client_id: str, ): """Get existing consent record for user-client pair.""" - return await oauth_consent_repo.get_consent(db, user_id=user_id, client_id=client_id) + return await oauth_consent_repo.get_consent( + db, user_id=user_id, client_id=client_id + ) async def check_consent( diff --git a/backend/app/services/oauth_service.py b/backend/app/services/oauth_service.py index 20c4e73..f023586 100644 --- a/backend/app/services/oauth_service.py +++ b/backend/app/services/oauth_service.py @@ -24,9 +24,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.auth import create_access_token, create_refresh_token from app.core.config import settings from app.core.exceptions import AuthenticationError +from app.models.user import User from app.repositories.oauth_account import oauth_account_repo as oauth_account from app.repositories.oauth_state import oauth_state_repo as oauth_state -from app.models.user import User from app.repositories.user import user_repo from app.schemas.oauth import ( OAuthAccountCreate, @@ -344,7 +344,9 @@ class OAuthService: await oauth_account.update_tokens( db, account=existing_oauth, - access_token=token.get("access_token"), refresh_token=token.get("refresh_token"), token_expires_at=datetime.now(UTC) + access_token=token.get("access_token"), + refresh_token=token.get("refresh_token"), + token_expires_at=datetime.now(UTC) + timedelta(seconds=token.get("expires_in", 3600)), ) @@ -373,7 +375,9 @@ class OAuthService: provider=provider, provider_user_id=provider_user_id, provider_email=provider_email, - access_token=token.get("access_token"), refresh_token=token.get("refresh_token"), token_expires_at=datetime.now(UTC) + access_token=token.get("access_token"), + refresh_token=token.get("refresh_token"), + token_expires_at=datetime.now(UTC) + timedelta(seconds=token.get("expires_in", 3600)) if token.get("expires_in") else None, @@ -639,7 +643,9 @@ class OAuthService: provider=provider, provider_user_id=provider_user_id, provider_email=email, - access_token=token.get("access_token"), refresh_token=token.get("refresh_token"), token_expires_at=datetime.now(UTC) + access_token=token.get("access_token"), + refresh_token=token.get("refresh_token"), + token_expires_at=datetime.now(UTC) + timedelta(seconds=token.get("expires_in", 3600)) if token.get("expires_in") else None, diff --git a/backend/app/services/organization_service.py b/backend/app/services/organization_service.py index de02311..245d979 100644 --- a/backend/app/services/organization_service.py +++ b/backend/app/services/organization_service.py @@ -51,9 +51,7 @@ class OrganizationService: """Permanently delete an organization by ID.""" await self._repo.remove(db, id=org_id) - async def get_member_count( - self, db: AsyncSession, *, organization_id: UUID - ) -> int: + async def get_member_count(self, db: AsyncSession, *, organization_id: UUID) -> int: """Get number of active members in an organization.""" return await self._repo.get_member_count(db, organization_id=organization_id) diff --git a/backend/app/services/session_service.py b/backend/app/services/session_service.py index a73590d..be0533b 100644 --- a/backend/app/services/session_service.py +++ b/backend/app/services/session_service.py @@ -25,7 +25,9 @@ class SessionService: """Create a new session record.""" return await self._repo.create_session(db, obj_in=obj_in) - async def get_session(self, db: AsyncSession, session_id: str) -> UserSession | None: + async def get_session( + self, db: AsyncSession, session_id: str + ) -> UserSession | None: """Get session by ID.""" return await self._repo.get(db, id=session_id) @@ -72,9 +74,7 @@ class SessionService: db, session=session, new_jti=new_jti, new_expires_at=new_expires_at ) - async def cleanup_expired_for_user( - self, db: AsyncSession, *, user_id: str - ) -> int: + async def cleanup_expired_for_user(self, db: AsyncSession, *, user_id: str) -> int: """Remove expired sessions for a user. Returns count removed.""" return await self._repo.cleanup_expired_for_user(db, user_id=user_id) diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 0ad787f..1ea2ccc 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -96,7 +96,9 @@ class UserService: await db.execute(select(func.count()).select_from(User)) ).scalar() or 0 active_count = ( - await db.execute(select(func.count()).select_from(User).where(User.is_active)) + await db.execute( + select(func.count()).select_from(User).where(User.is_active) + ) ).scalar() or 0 inactive_count = ( await db.execute( @@ -104,9 +106,7 @@ class UserService: ) ).scalar() or 0 all_users = list( - ( - await db.execute(select(User).order_by(User.created_at)) - ).scalars().all() + (await db.execute(select(User).order_by(User.created_at))).scalars().all() ) return { "total_users": total_users, diff --git a/backend/tests/api/test_auth_security.py b/backend/tests/api/test_auth_security.py index 773d221..60b755d 100644 --- a/backend/tests/api/test_auth_security.py +++ b/backend/tests/api/test_auth_security.py @@ -12,8 +12,8 @@ These tests prevent real-world attack scenarios. import pytest from httpx import AsyncClient -from app.repositories.session import session_repo as session_crud from app.models.user import User +from app.repositories.session import session_repo as session_crud class TestRevokedSessionSecurity: diff --git a/backend/tests/api/test_permissions_security.py b/backend/tests/api/test_permissions_security.py index 1526d23..c26165e 100644 --- a/backend/tests/api/test_permissions_security.py +++ b/backend/tests/api/test_permissions_security.py @@ -11,9 +11,9 @@ These tests prevent unauthorized access and privilege escalation. import pytest from httpx import AsyncClient -from app.repositories.user import user_repo as user_crud from app.models.organization import Organization from app.models.user import User +from app.repositories.user import user_repo as user_crud class TestInactiveUserBlocking: diff --git a/backend/tests/api/test_users.py b/backend/tests/api/test_users.py index f74dadb..b4673ad 100644 --- a/backend/tests/api/test_users.py +++ b/backend/tests/api/test_users.py @@ -99,7 +99,8 @@ class TestUpdateCurrentUser: from unittest.mock import patch with patch( - "app.api.routes.users.user_service.update_user", side_effect=Exception("DB error") + "app.api.routes.users.user_service.update_user", + side_effect=Exception("DB error"), ): with pytest.raises(Exception): await client.patch( @@ -224,7 +225,8 @@ class TestUpdateUserById: from unittest.mock import patch with patch( - "app.api.routes.users.user_service.update_user", side_effect=ValueError("Invalid") + "app.api.routes.users.user_service.update_user", + side_effect=ValueError("Invalid"), ): with pytest.raises(ValueError): await client.patch( @@ -241,7 +243,8 @@ class TestUpdateUserById: from unittest.mock import patch with patch( - "app.api.routes.users.user_service.update_user", side_effect=Exception("Unexpected") + "app.api.routes.users.user_service.update_user", + side_effect=Exception("Unexpected"), ): with pytest.raises(Exception): await client.patch( diff --git a/backend/tests/repositories/test_base.py b/backend/tests/repositories/test_base.py index 8e98a4b..b388e5d 100644 --- a/backend/tests/repositories/test_base.py +++ b/backend/tests/repositories/test_base.py @@ -170,7 +170,9 @@ class TestCRUDBaseCreate: last_name="User", ) - with pytest.raises(DuplicateEntryError, match="Database integrity error"): + with pytest.raises( + DuplicateEntryError, match="Database integrity error" + ): await user_crud.create(session, obj_in=user_data) @pytest.mark.asyncio @@ -307,7 +309,9 @@ class TestCRUDBaseUpdate: "statement", {}, Exception("constraint failed") ), ): - with pytest.raises(IntegrityConstraintError, match="Database integrity error"): + with pytest.raises( + IntegrityConstraintError, match="Database integrity error" + ): await user_crud.update( session, db_obj=user, obj_in={"first_name": "Test"} ) @@ -327,7 +331,9 @@ class TestCRUDBaseUpdate: "statement", {}, Exception("connection error") ), ): - with pytest.raises(IntegrityConstraintError, match="Database operation failed"): + with pytest.raises( + IntegrityConstraintError, match="Database operation failed" + ): await user_crud.update( session, db_obj=user, obj_in={"first_name": "Test"} ) @@ -408,7 +414,8 @@ class TestCRUDBaseRemove: ), ): with pytest.raises( - IntegrityConstraintError, match="Cannot delete.*referenced by other records" + IntegrityConstraintError, + match="Cannot delete.*referenced by other records", ): await user_crud.remove(session, id=str(async_test_user.id)) @@ -904,8 +911,8 @@ class TestCRUDBaseModelsWithoutSoftDelete: _test_engine, SessionLocal = async_test_db # Create an organization (which doesn't have deleted_at) - from app.repositories.organization import organization_repo as org_crud from app.models.organization import Organization + from app.repositories.organization import organization_repo as org_crud async with SessionLocal() as session: org = Organization(name="Test Org", slug="test-org") @@ -915,7 +922,9 @@ class TestCRUDBaseModelsWithoutSoftDelete: # Try to soft delete organization (should fail) async with SessionLocal() as session: - with pytest.raises(InvalidInputError, match="does not have a deleted_at column"): + with pytest.raises( + InvalidInputError, match="does not have a deleted_at column" + ): await org_crud.soft_delete(session, id=str(org_id)) @pytest.mark.asyncio @@ -924,8 +933,8 @@ class TestCRUDBaseModelsWithoutSoftDelete: _test_engine, SessionLocal = async_test_db # Create an organization (which doesn't have deleted_at) - from app.repositories.organization import organization_repo as org_crud from app.models.organization import Organization + from app.repositories.organization import organization_repo as org_crud async with SessionLocal() as session: org = Organization(name="Restore Test", slug="restore-test") @@ -935,7 +944,9 @@ class TestCRUDBaseModelsWithoutSoftDelete: # Try to restore organization (should fail) async with SessionLocal() as session: - with pytest.raises(InvalidInputError, match="does not have a deleted_at column"): + with pytest.raises( + InvalidInputError, match="does not have a deleted_at column" + ): await org_crud.restore(session, id=str(org_id)) @@ -955,8 +966,8 @@ class TestCRUDBaseEagerLoadingWithRealOptions: _test_engine, SessionLocal = async_test_db # Create a session for the user - from app.repositories.session import session_repo as session_crud from app.models.user_session import UserSession + from app.repositories.session import session_repo as session_crud async with SessionLocal() as session: user_session = UserSession( @@ -994,8 +1005,8 @@ class TestCRUDBaseEagerLoadingWithRealOptions: _test_engine, SessionLocal = async_test_db # Create multiple sessions for the user - from app.repositories.session import session_repo as session_crud from app.models.user_session import UserSession + from app.repositories.session import session_repo as session_crud async with SessionLocal() as session: for i in range(3): diff --git a/backend/tests/repositories/test_base_db_failures.py b/backend/tests/repositories/test_base_db_failures.py index e468062..4e939b5 100644 --- a/backend/tests/repositories/test_base_db_failures.py +++ b/backend/tests/repositories/test_base_db_failures.py @@ -120,7 +120,9 @@ class TestBaseCRUDUpdateFailures: with patch.object( session, "rollback", new_callable=AsyncMock ) as mock_rollback: - with pytest.raises(IntegrityConstraintError, match="Database operation failed"): + with pytest.raises( + IntegrityConstraintError, match="Database operation failed" + ): await user_crud.update( session, db_obj=user, obj_in={"first_name": "Updated"} ) @@ -142,7 +144,9 @@ class TestBaseCRUDUpdateFailures: with patch.object( session, "rollback", new_callable=AsyncMock ) as mock_rollback: - with pytest.raises(IntegrityConstraintError, match="Database operation failed"): + with pytest.raises( + IntegrityConstraintError, match="Database operation failed" + ): await user_crud.update( session, db_obj=user, obj_in={"first_name": "Updated"} ) diff --git a/backend/tests/repositories/test_oauth.py b/backend/tests/repositories/test_oauth.py index 45a8077..5905645 100644 --- a/backend/tests/repositories/test_oauth.py +++ b/backend/tests/repositories/test_oauth.py @@ -63,7 +63,8 @@ class TestOAuthAccountCRUD: # SQLite returns different error message than PostgreSQL with pytest.raises( - DuplicateEntryError, match="(already linked|UNIQUE constraint failed|Failed to create)" + DuplicateEntryError, + match="(already linked|UNIQUE constraint failed|Failed to create)", ): await oauth_account.create_account(session, obj_in=account_data2) diff --git a/backend/tests/repositories/test_organization.py b/backend/tests/repositories/test_organization.py index d544320..7670cda 100644 --- a/backend/tests/repositories/test_organization.py +++ b/backend/tests/repositories/test_organization.py @@ -10,9 +10,9 @@ import pytest from sqlalchemy import select from app.core.repository_exceptions import DuplicateEntryError, IntegrityConstraintError -from app.repositories.organization import organization_repo as organization_crud from app.models.organization import Organization from app.models.user_organization import OrganizationRole, UserOrganization +from app.repositories.organization import organization_repo as organization_crud from app.schemas.organizations import OrganizationCreate @@ -973,7 +973,9 @@ class TestOrganizationExceptionHandlers: with patch.object(session, "commit", side_effect=mock_commit): with patch.object(session, "rollback", new_callable=AsyncMock): org_in = OrganizationCreate(name="Test", slug="test") - with pytest.raises(IntegrityConstraintError, match="Database integrity error"): + with pytest.raises( + IntegrityConstraintError, match="Database integrity error" + ): await organization_crud.create(session, obj_in=org_in) @pytest.mark.asyncio @@ -1059,7 +1061,8 @@ class TestOrganizationExceptionHandlers: with patch.object(session, "commit", side_effect=mock_commit): with patch.object(session, "rollback", new_callable=AsyncMock): with pytest.raises( - IntegrityConstraintError, match="Failed to add user to organization" + IntegrityConstraintError, + match="Failed to add user to organization", ): await organization_crud.add_user( session, diff --git a/backend/tests/repositories/test_session.py b/backend/tests/repositories/test_session.py index 0c1c902..3f22063 100644 --- a/backend/tests/repositories/test_session.py +++ b/backend/tests/repositories/test_session.py @@ -9,8 +9,8 @@ from uuid import uuid4 import pytest from app.core.repository_exceptions import InvalidInputError -from app.repositories.session import session_repo as session_crud from app.models.user_session import UserSession +from app.repositories.session import session_repo as session_crud from app.schemas.sessions import SessionCreate diff --git a/backend/tests/repositories/test_session_db_failures.py b/backend/tests/repositories/test_session_db_failures.py index 9bf3ef9..2271a04 100644 --- a/backend/tests/repositories/test_session_db_failures.py +++ b/backend/tests/repositories/test_session_db_failures.py @@ -11,8 +11,8 @@ import pytest from sqlalchemy.exc import OperationalError from app.core.repository_exceptions import IntegrityConstraintError -from app.repositories.session import session_repo as session_crud from app.models.user_session import UserSession +from app.repositories.session import session_repo as session_crud from app.schemas.sessions import SessionCreate @@ -103,7 +103,9 @@ class TestSessionCRUDCreateSessionFailures: last_used_at=datetime.now(UTC), ) - with pytest.raises(IntegrityConstraintError, match="Failed to create session"): + with pytest.raises( + IntegrityConstraintError, match="Failed to create session" + ): await session_crud.create_session(session, obj_in=session_data) mock_rollback.assert_called_once() @@ -134,7 +136,9 @@ class TestSessionCRUDCreateSessionFailures: last_used_at=datetime.now(UTC), ) - with pytest.raises(IntegrityConstraintError, match="Failed to create session"): + with pytest.raises( + IntegrityConstraintError, match="Failed to create session" + ): await session_crud.create_session(session, obj_in=session_data) mock_rollback.assert_called_once() diff --git a/backend/tests/services/test_organization_service.py b/backend/tests/services/test_organization_service.py index 681a813..67e4cf8 100644 --- a/backend/tests/services/test_organization_service.py +++ b/backend/tests/services/test_organization_service.py @@ -4,12 +4,11 @@ import uuid import pytest -import pytest_asyncio from app.core.exceptions import NotFoundError from app.models.user_organization import OrganizationRole from app.schemas.organizations import OrganizationCreate, OrganizationUpdate -from app.services.organization_service import OrganizationService, organization_service +from app.services.organization_service import organization_service def _make_org_create(name=None, slug=None) -> OrganizationCreate: @@ -50,9 +49,7 @@ class TestGetOrganization: _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: with pytest.raises(NotFoundError): - await organization_service.get_organization( - session, str(uuid.uuid4()) - ) + await organization_service.get_organization(session, str(uuid.uuid4())) class TestCreateOrganization: diff --git a/backend/tests/services/test_session_service.py b/backend/tests/services/test_session_service.py index e6dfb2b..5be3c93 100644 --- a/backend/tests/services/test_session_service.py +++ b/backend/tests/services/test_session_service.py @@ -5,10 +5,9 @@ import uuid from datetime import UTC, datetime, timedelta import pytest -import pytest_asyncio from app.schemas.sessions import SessionCreate -from app.services.session_service import SessionService, session_service +from app.services.session_service import session_service def _make_session_create(user_id, jti=None) -> SessionCreate: diff --git a/backend/tests/services/test_user_service.py b/backend/tests/services/test_user_service.py index 47ab065..f1d205c 100644 --- a/backend/tests/services/test_user_service.py +++ b/backend/tests/services/test_user_service.py @@ -4,13 +4,12 @@ import uuid import pytest -import pytest_asyncio from sqlalchemy import select from app.core.exceptions import NotFoundError from app.models.user import User from app.schemas.users import UserCreate, UserUpdate -from app.services.user_service import UserService, user_service +from app.services.user_service import user_service class TestGetUser: