Skip to content

Commit

Permalink
DESENG-436: MET - Rewrite unit tests (#2358)
Browse files Browse the repository at this point in the history
* DESENG-436: MET - Rewrite unit tests (#1)

Co-authored-by: Baelx <[email protected]>

* Update met-api/tests/unit/api/test_engagement.py

Co-authored-by: Baelx <[email protected]>

---------

Co-authored-by: Baelx <[email protected]>
  • Loading branch information
VineetBala-AOT and Baelx authored Jan 16, 2024
1 parent 4d09511 commit 027366f
Show file tree
Hide file tree
Showing 35 changed files with 341 additions and 191 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/met-api-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,23 @@ jobs:
JWT_OIDC_TEST_CLIENT_SECRET: "1111111111"
JWT_OIDC_TEST_JWKS_CACHE_TIMEOUT: "6000"


KEYCLOAK_ADMIN_CLIENTID: "met-admin"
KEYCLOAK_ADMIN_SECRET: "2222222222"
KEYCLOAK_AUTH_AUDIENCE: "met-web"
KEYCLOAK_AUTH_CLIENT_SECRET: "1111111111"
KEYCLOAK_BASE_URL: "http://localhost:8081/auth"
KEYCLOAK_REALMNAME: "demo"
USE_KEYCLOAK_DOCKER: "YES"

KEYCLOAK_TEST_ADMIN_CLIENTID: "met-admin"
KEYCLOAK_TEST_ADMIN_SECRET: "2222222222"
KEYCLOAK_TEST_AUTH_AUDIENCE: "met-web"
KEYCLOAK_TEST_AUTH_CLIENT_SECRET: "1111111111"
KEYCLOAK_TEST_BASE_URL: "http://localhost:8081"
KEYCLOAK_TEST_REALMNAME: "demo"
USE_TEST_KEYCLOAK_DOCKER: "YES"
SQLALCHEMY_DATABASE_URI: "postgresql://postgres:postgres@localhost:5432/postgres"

runs-on: ubuntu-20.04

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## January 15, 2024

- **Task** Audit for missing unit tests [🎟️DESENG-436](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-436)
- Corrected tests to execute smoothly in the local environment using development variables.
- Established continuous integration configuration to automatically run tests upon each commit.
- Ensured that the tests pass successfully in our GitHub test environment.
- **Task** Audit for missing unit tests [🎟️DESENG-449](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-449)
- Identified and corrected failing unit test for api and web
- **Bug Fix**: Fixing Form.io error when reordering Radio Button options [🎟️DESENG-446](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-446)

## January 10, 2024
Expand Down
2 changes: 1 addition & 1 deletion analytics-api/src/analytics_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class _Config(): # pylint: disable=too-few-public-methods
JWT_OIDC_JWKS_CACHE_TIMEOUT = 300

# default tenant configs ; Set to EAO for now.Overwrite using openshift variables
DEFAULT_TENANT_SHORT_NAME = os.getenv('DEFAULT_TENANT_SHORT_NAME', 'EAO')
DEFAULT_TENANT_SHORT_NAME = os.getenv('DEFAULT_TENANT_SHORT_NAME', 'GDX')
DEFAULT_TENANT_NAME = os.getenv('DEFAULT_TENANT_NAME', 'Environment Assessment Office')
DEFAULT_TENANT_DESCRIPTION = os.getenv('DEFAULT_TENANT_DESCRIPTION', 'Environment Assessment Office')

Expand Down
2 changes: 2 additions & 0 deletions met-api/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ DATABASE_TEST_NAME=
DATABASE_TEST_HOST=
DATABASE_TEST_PORT=

KEYCLOAK_TEST_BASE_URL="http://localhost:8081"

# Docker database settings
# If unset, uses the same settings as the main database
DATABASE_DOCKER_USERNAME=
Expand Down
1 change: 1 addition & 0 deletions met-api/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ max-line-length = 120
docstring-min-length=10
per-file-ignores =
*/__init__.py:F401
*/config.py:N802

[pycodestyle]
max_line_length = 120
Expand Down
4 changes: 4 additions & 0 deletions met-api/src/met_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def __init__(self) -> None:
os.environ['FLASK_DEBUG'] = str(self.USE_DEBUG)

@property
# pylint: disable=invalid-name
def SQLALCHEMY_DATABASE_URI(self) -> str:
"""
Dynamically fetch the SQLAlchemy Database URI based on the DB config.
Expand Down Expand Up @@ -307,6 +308,8 @@ def SQLALCHEMY_DATABASE_URI(self) -> str:
'TOKEN_URL': os.getenv('CDOGS_TOKEN_URL'),
}

PROPAGATE_EXCEPTIONS = True


class DevConfig(Config): # pylint: disable=too-few-public-methods
"""Dev Config."""
Expand Down Expand Up @@ -368,6 +371,7 @@ def __init__(self) -> None:
'HOST': os.getenv('DATABASE_TEST_HOST', Config.DB.get('HOST')),
'PORT': os.getenv('DATABASE_TEST_PORT', Config.DB.get('PORT')),
}
IS_SINGLE_TENANT_ENVIRONMENT = False


class DockerConfig(Config): # pylint: disable=too-few-public-methods
Expand Down
2 changes: 1 addition & 1 deletion met-api/src/met_api/constants/timeline_event_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ class TimelineEventStatus(IntEnum):

Pending = 1
InProgress = 2
Completed = 3
Completed = 3
16 changes: 1 addition & 15 deletions met-api/src/met_api/models/widget_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
Manages the timeline widget
"""
from __future__ import annotations
from typing import Optional
from sqlalchemy.sql.schema import ForeignKey
from met_api.models.timeline_event import TimelineEvent
from met_api.services.timeline_event_service import TimelineEventService
from .base_model import BaseModel
from .db import db


class WidgetTimeline(BaseModel): # pylint: disable=too-few-public-methods, too-many-instance-attributes
"""Definition of the Timeline entity."""

Expand All @@ -30,16 +29,3 @@ def get_timeline(cls, timeline_id) -> list[WidgetTimeline]:
.filter(WidgetTimeline.widget_id == timeline_id) \
.all()
return widget_timeline

@classmethod
def update_timeline(cls, timeline_id, timeline_data: dict) -> Optional[WidgetTimeline or None]:
"""Update timeline."""
TimelineEvent.delete_event(timeline_id)
widget_timeline: WidgetTimeline = WidgetTimeline.query.get(timeline_id)
if widget_timeline:
widget_timeline.title = timeline_data.get('title')
widget_timeline.description = timeline_data.get('description')
for event in timeline_data.get('events', []):
TimelineEventService.create_timeline_event(timeline_id, event)
widget_timeline.save()
return widget_timeline
5 changes: 4 additions & 1 deletion met-api/src/met_api/schemas/widget_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
from marshmallow import Schema
from marshmallow_sqlalchemy.fields import Nested


class TimelineEventSchema(Schema): # pylint: disable=too-many-ancestors, too-few-public-methods
"""This is the schema for the timeline event model."""

class Meta: # pylint: disable=too-few-public-methods
"""All of the fields in the Timeline Event schema."""
Expand All @@ -29,11 +31,12 @@ class Meta: # pylint: disable=too-few-public-methods


class WidgetTimelineSchema(Schema): # pylint: disable=too-many-ancestors, too-few-public-methods
"""This is the schema for the widget timeline model."""

class Meta: # pylint: disable=too-few-public-methods
"""All of the fields in the Widget Timeline schema."""

model = WidgetTimelineModel
fields = ('id', 'engagement_id', 'widget_id', 'title', 'description', 'events')

events = Nested(TimelineEventSchema, many=True)
6 changes: 3 additions & 3 deletions met-api/src/met_api/services/keycloak.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ def get_user_groups(user_id):
def get_users_groups(user_ids: List):
"""Get user groups from Keycloak by user ids.For bulk purposes."""
# TODO if List is bigger than a number ; if so reject.
base_url = current_app.config.get('KEYCLOAK_BASE_URL')
keycloak = current_app.config['KEYCLOAK_CONFIG']
base_url = keycloak['BASE_URL']
# TODO fix this during tests and remove below
if not base_url:
return {}
Expand Down Expand Up @@ -106,12 +107,12 @@ def _get_admin_token():
admin_client_id = keycloak['ADMIN_USERNAME']
admin_secret = keycloak['ADMIN_SECRET']
timeout = keycloak['CONNECT_TIMEOUT']

headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
token_issuer = current_app.config['JWT_CONFIG']['ISSUER']
token_url = f'{token_issuer}/protocol/openid-connect/token'

response = requests.post(
token_url,
headers=headers,
Expand Down Expand Up @@ -243,7 +244,6 @@ def get_user_by_username(username, admin_token=None):
'Content-Type': ContentType.JSON.value,
'Authorization': f'Bearer {admin_token}'
}

# Get the user and return
query_user_url = f'{base_url}/auth/admin/realms/{realm}/users?username={username}'
response = requests.get(query_user_url, headers=headers, timeout=timeout)
Expand Down
60 changes: 45 additions & 15 deletions met-api/src/met_api/services/widget_timeline_service.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Service for Widget Timeline management."""
from http import HTTPStatus
from typing import Optional

from met_api.constants.membership_type import MembershipType
from met_api.exceptions.business_exception import BusinessException
from met_api.models.widget_timeline import WidgetTimeline as WidgetTimelineModel
from met_api.models.timeline_event import TimelineEvent as TimelineEventModel
from met_api.services import authorization
from met_api.services.timeline_event_service import TimelineEventService
from met_api.utils.roles import Role


Expand All @@ -28,23 +33,48 @@ def create_timeline(widget_id: int, timeline_details: dict):
return widget_timeline

@staticmethod
def update_timeline(widget_id: int, timeline_id: int, timeline_data: dict):
def update_timeline(widget_id: int, timeline_id: int, timeline_data: dict) -> Optional[WidgetTimelineModel]:
"""Update timeline widget."""
events = timeline_data.get("events")
events = timeline_data.get('events')
first_event = events[0]

WidgetTimelineService._check_update_timeline_auth(first_event)

widget_timeline: WidgetTimelineModel = WidgetTimelineModel.find_by_id(timeline_id)
authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
Role.EDIT_ENGAGEMENT.value), engagement_id=first_event.get('engagement_id'))

if not widget_timeline:
raise KeyError('Timeline widget not found')
raise BusinessException(
error='Timeline widget not found',
status_code=HTTPStatus.BAD_REQUEST)

if widget_timeline.widget_id != widget_id:
raise ValueError('Invalid widget ID')
raise BusinessException(
error='Invalid widget ID',
status_code=HTTPStatus.BAD_REQUEST)

if widget_timeline.id != timeline_id:
raise ValueError('Invalid timeline ID')
raise BusinessException(
error='Invalid timeline ID',
status_code=HTTPStatus.BAD_REQUEST)

WidgetTimelineService._update_widget_timeline(widget_timeline, timeline_data)

return widget_timeline

return WidgetTimelineModel.update_timeline(timeline_id, timeline_data)
@staticmethod
def _check_update_timeline_auth(first_event):
eng_id = first_event.get('engagement_id')
authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id)

@staticmethod
def _update_widget_timeline(widget_timeline: WidgetTimelineModel, timeline_data: dict):
widget_timeline.title = timeline_data.get('title')
widget_timeline.description = timeline_data.get('description')
TimelineEventModel.delete_event(widget_timeline.id)
for event in timeline_data.get('events', []):
TimelineEventService.create_timeline_event(widget_timeline.id, event)
widget_timeline.save()

@staticmethod
def _create_timeline_model(widget_id: int, timeline_data: dict):
Expand All @@ -56,13 +86,13 @@ def _create_timeline_model(widget_id: int, timeline_data: dict):
for event in timeline_data.get('events', []):
timeline_model.events.append(
TimelineEventModel(
widget_id = widget_id,
engagement_id = event.get('engagement_id'),
timeline_id = event.get('timeline_id'),
description = event.get('description'),
time = event.get('time'),
position = event.get('position'),
status = event.get('status'),
widget_id=widget_id,
engagement_id=event.get('engagement_id'),
timeline_id=event.get('timeline_id'),
description=event.get('description'),
time=event.get('time'),
position=event.get('position'),
status=event.get('status'),
)
)
timeline_model.flush()
Expand Down
51 changes: 51 additions & 0 deletions met-api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
import time
from random import random

import copy
import pytest
from flask_migrate import Migrate, upgrade
from sqlalchemy import event, text

from met_api import create_app, setup_jwt_manager
from met_api.auth import jwt as _jwt
from met_api.models import db as _db
from tests.utilities.factory_utils import factory_staff_user_model
from tests.utilities.factory_scenarios import TestJwtClaims, TestUserInfo


@pytest.fixture(scope='session')
Expand Down Expand Up @@ -169,3 +172,51 @@ def docker_compose_files(pytestconfig):
def auth_mock(monkeypatch):
"""Mock check_auth."""
pass


# Fixture for setting up user and claims for an admin user
@pytest.fixture
def setup_admin_user_and_claims(jwt):
"""Set up a user with the staff admin role."""
staff_info = dict(TestUserInfo.user_staff_1)
user = factory_staff_user_model(user_info=staff_info)
claims = copy.deepcopy(TestJwtClaims.staff_admin_role.value)
claims['sub'] = str(user.external_id)

return user, claims


# Fixture for setting up user and claims for a reviewer
@pytest.fixture
def setup_reviewer_and_claims(jwt):
"""Set up a user with the reviewer role."""
staff_info = dict(TestUserInfo.user_staff_1)
user = factory_staff_user_model(user_info=staff_info)
claims = copy.deepcopy(TestJwtClaims.reviewer_role.value)
claims['sub'] = str(user.external_id)

return user, claims


# Fixture for setting up user and claims for a team member
@pytest.fixture
def setup_team_member_and_claims(jwt):
"""Set up a user with the team member role."""
staff_info = dict(TestUserInfo.user_staff_1)
user = factory_staff_user_model(user_info=staff_info)
claims = copy.deepcopy(TestJwtClaims.team_member_role.value)
claims['sub'] = str(user.external_id)

return user, claims


# Fixture for setting up user and claims for a user with no role
@pytest.fixture
def setup_unprivileged_user_and_claims(jwt):
"""Set up a user with the no role."""
staff_info = dict(TestUserInfo.user_staff_1)
user = factory_staff_user_model(user_info=staff_info)
claims = copy.deepcopy(TestJwtClaims.no_role.value)
claims['sub'] = str(user.external_id)

return user, claims
5 changes: 3 additions & 2 deletions met-api/tests/unit/api/test_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,10 @@ def test_review_comment_review_note(client, jwt, session): # pylint:disable=unu
mock_mail.assert_called()


def test_get_comments_spreadsheet(mocker, client, jwt, session): # pylint:disable=unused-argument
def test_get_comments_spreadsheet(mocker, client, jwt, session,
setup_admin_user_and_claims): # pylint:disable=unused-argument
"""Assert that comments sheet can be fetched."""
claims = TestJwtClaims.staff_admin_role
user, claims = setup_admin_user_and_claims

mock_post_generate_document_response = MagicMock()
mock_post_generate_document_response.content = b'mock data'
Expand Down
Loading

0 comments on commit 027366f

Please sign in to comment.