diff --git a/README.md b/README.md
index d27fe22..3974b50 100644
--- a/README.md
+++ b/README.md
@@ -111,13 +111,12 @@ object, with the user's profile information if requested.
#### Request Parameters
-| **Parameter** | **Optional** | **Type** | **Default** | **Description** |
-| ------------------------- | ------------ | ----------- | ----------- | ----------------------------------------------------------------------------------------------- |
-| `username` | No | `str` | | The user's SRN or PRN |
-| `password` | No | `str` | | The user's password |
-| `profile` | Yes | `boolean` | `False` | Whether to fetch profile information |
-| `knowYourClassAndSection` | Yes | `boolean` | `False` | Whether to fetch data from PESU's "Know Your Class and Section" information |
-| `fields` | Yes | `list[str]` | `None` | Which fields to fetch from the profile information. If not provided, all fields will be fetched |
+| **Parameter** | **Optional** | **Type** | **Default** | **Description** |
+| ------------- | ------------ | ----------- | ----------- | ----------------------------------------------------------------------------------------------- |
+| `username` | No | `str` | | The user's SRN or PRN |
+| `password` | No | `str` | | The user's password |
+| `profile` | Yes | `boolean` | `False` | Whether to fetch profile information |
+| `fields` | Yes | `list[str]` | `None` | Which fields to fetch from the profile information. If not provided, all fields will be fetched |
#### Response Object
@@ -125,13 +124,12 @@ On authentication, it returns the following parameters in a JSON object. If the
profile data was requested, the response's `profile` key will store a dictionary with a user's profile information.
**On an unsuccessful sign-in, this field will not exist**.
-| **Field** | **Type** | **Description** |
-| ------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
-| `status` | `boolean` | A flag indicating whether the overall request was successful |
-| `profile` | `ProfileObject` | A nested map storing the profile information, returned only if requested |
-| `knowYourClassAndSection` | `KnowYourClassAndSectionObject` | A nested map storing the profile information from PESU's "Know Your Class and Section" endpoint |
-| `message` | `str` | A message that provides information corresponding to the status |
-| `timestamp` | `datetime` | A timezone offset timestamp indicating the time of authentication |
+| **Field** | **Type** | **Description** |
+| ----------- | --------------- | ------------------------------------------------------------------------ |
+| `status` | `boolean` | A flag indicating whether the overall request was successful |
+| `profile` | `ProfileObject` | A nested map storing the profile information, returned only if requested |
+| `message` | `str` | A message that provides information corresponding to the status |
+| `timestamp` | `datetime` | A timezone offset timestamp indicating the time of authentication |
##### `ProfileObject`
@@ -152,21 +150,6 @@ If the authentication fails, this field will not be present in the response.
| `campusCode` | The integer code of the campus (1 for RR and 2 for EC) |
| `campus` | Abbreviation of the user's campus name |
-#### `KnowYourClassAndSectionObject`
-
-| **Field** | **Description** |
-| --------------- | ------------------------------------------------------------------------ |
-| `prn` | PRN of the user |
-| `srn` | SRN of the user |
-| `name` | Name of the user |
-| `semester` | Current semester that the user is in |
-| `section` | Section of the user |
-| `cycle` | Physics Cycle or Chemistry Cycle, if the user is in first year |
-| `department` | Abbreviation of the branch along with the campus the user is studying in |
-| `branch` | Abbreviation of the branch that the user is pursuing |
-| `instituteName` | The name of the campus that the user is studying in |
-| `error` | The error name and stack trace, if an error occurs |
-
### `/health`
This endpoint can be used to check the health of the API. It's useful for monitoring and uptime checks. This endpoint
@@ -199,7 +182,6 @@ data = {
"username": "your SRN or PRN here",
"password": "your password here",
"profile": True, # Optional, defaults to False
- 'knowYourClassAndSection': True, # Optional, defaults to False
}
response = requests.post("http://localhost:5000/authenticate", json=data)
@@ -225,17 +207,6 @@ print(response.json())
"campus": "RR"
},
"message": "Login successful.",
- "knowYourClassAndSection": {
- "prn": "PES1201800001",
- "srn": "PES1201800001",
- "name": "JOHNNY BLAZE",
- "semester": "Sem-8",
- "section": "Section F",
- "cycle": "NA",
- "department": "CSE(EC Campus)",
- "branch": "CSE",
- "instituteName": "PES University (Electronic City)"
- },
"timestamp": "2024-07-28 22:30:10.103368+05:30"
}
```
diff --git a/app/app.py b/app/app.py
index 7998c9a..7fc99d08 100644
--- a/app/app.py
+++ b/app/app.py
@@ -202,7 +202,6 @@ async def authenticate(payload: RequestModel, background_tasks: BackgroundTasks)
username = payload.username
password = payload.password
profile = payload.profile
- know_your_class_and_section = payload.know_your_class_and_section
fields = payload.fields
# Authenticate the user
@@ -213,7 +212,6 @@ async def authenticate(payload: RequestModel, background_tasks: BackgroundTasks)
username=username,
password=password,
profile=profile,
- know_your_class_and_section=know_your_class_and_section,
fields=fields,
),
)
diff --git a/app/docs/authenticate.py b/app/docs/authenticate.py
index a8acd47..39ef84f 100644
--- a/app/docs/authenticate.py
+++ b/app/docs/authenticate.py
@@ -23,16 +23,6 @@
"profile": True,
},
},
- "auth_with_kycas": {
- "summary": 'Authentication with "Know Your Class and Section" endpoint',
- "description": 'Authentication with "Know Your Class and Section" data',
- "value": {
- "username": "PES1201800001",
- "password": "mySecurePassword123",
- "profile": True,
- "knowYourClassAndSection": True,
- },
- },
"phone_auth_selective_fields": {
"summary": "Authentication with Selected Fields",
"description": "Authentication using username and requesting specific profile data fields",
@@ -84,38 +74,6 @@
},
},
},
- "authentication_with_kycas": {
- "summary": 'Authentication with "Know Your Class and Section" data"',
- "value": {
- "status": True,
- "message": "Login successful.",
- "timestamp": "2024-07-28T22:30:10.103368+05:30",
- "profile": {
- "name": "John Doe",
- "prn": "PESXXYYZZZZZ",
- "srn": "PESXXUGYYZZZ",
- "program": "Bachelor of Technology",
- "branch": "Computer Science and Engineering",
- "semester": "2",
- "section": "C",
- "email": "johndoe@gmail.com",
- "phone": "1234567890",
- "campusCode": 1,
- "campus": "RR",
- },
- "knowYourClassAndSection": {
- "prn": "PESXXYYZZZZZ",
- "srn": "PESXXUGYYZZZ",
- "name": "John Doe",
- "semester": "Sem-X",
- "section": "Section X",
- "cycle": "NA",
- "department": "Computer Science and Engineering",
- "branch": "CSE",
- "instituteName": "PES University",
- },
- },
- },
"authentication_with_selected_fields": {
"summary": "Authentication with Selected Fields",
"value": {
@@ -209,14 +167,6 @@
"timestamp": "2024-07-28T22:30:10.103368+05:30",
},
},
- "kycas_fetch_error": {
- "summary": '"Know Your Class and Section" endpoint fetching failed',
- "value": {
- "status": False,
- "message": "Failed to fetch Know Your Class and Section data from PESU Academy.",
- "timestamp": "2024-07-28T22:30:10.103368+05:30",
- },
- },
}
}
},
diff --git a/app/exceptions/authentication.py b/app/exceptions/authentication.py
index 62560d8..03833c8 100644
--- a/app/exceptions/authentication.py
+++ b/app/exceptions/authentication.py
@@ -33,14 +33,3 @@ class ProfileParseError(PESUAcademyError):
def __init__(self, message: str = "Failed to parse student profile page from PESU Academy.") -> None:
"""Initialize the ProfileParseError with a custom message."""
super().__init__(message, status_code=422)
-
-
-class KYCASFetchError(PESUAcademyError):
- """Raised when "Know Your Class and Section" data could not be fetched from PESU Academy."""
-
- def __init__(
- self,
- message: str = 'Failed to fetch "Know Your Class and Section" data from PESU Academy.',
- ) -> None:
- """Initialize the "Know Your Class and Section" FetchError with a custom message."""
- super().__init__(message, status_code=502)
diff --git a/app/models/__init__.py b/app/models/__init__.py
index 3791feb..c06043f 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -1,6 +1,5 @@
"""Custom models for the PESUAuth API."""
-from .kycas import KYCASModel as KYCASModel
from .profile import ProfileModel as ProfileModel
from .request import RequestModel as RequestModel
from .response import ResponseModel as ResponseModel
diff --git a/app/models/kycas.py b/app/models/kycas.py
deleted file mode 100644
index 4934a36..0000000
--- a/app/models/kycas.py
+++ /dev/null
@@ -1,65 +0,0 @@
-"""Model representing the "Know Your Class and Section" data returned after successful authentication."""
-
-from pydantic import BaseModel, ConfigDict, Field
-from pydantic.alias_generators import to_camel
-
-
-class KYCASModel(BaseModel):
- """Model representing the "Know Your Class and Section" data."""
-
- model_config = ConfigDict(strict=True, alias_generator=to_camel, populate_by_name=True)
-
- prn: str | None = Field(
- None,
- title="PRN",
- description="PRN of the user.",
- json_schema_extra={"example": "PESXXYYZZZZZ"},
- )
- srn: str | None = Field(
- None,
- title="SRN",
- description="SRN of the user.",
- json_schema_extra={"example": "PESXXUGYYZZZ"},
- )
- name: str | None = Field(
- None,
- title="Name",
- description="Full name of the user.",
- json_schema_extra={"example": "John Doe"},
- )
- semester: str | None = Field(
- None,
- title="Semester",
- description="Semester the user belongs to.",
- json_schema_extra={"example": "Sem-X"},
- )
- section: str | None = Field(
- None,
- title="Section",
- description="Section the user belongs to.",
- json_schema_extra={"example": "Section X"},
- )
- cycle: str | None = Field(
- None,
- title="Cycle",
- description="Cycle the user belongs to.",
- json_schema_extra={"example": "NA"},
- )
- department: str | None = Field(
- None,
- title="Department",
- description="Department the user belongs to.",
- json_schema_extra={"example": "Computer Science and Engineering"},
- )
- branch: str | None = Field(
- None,
- title="Branch",
- description="Abbreviation of the branch that the user is pursuing.",
- json_schema_extra={"example": "CSE"},
- )
- institute_name: str | None = Field(
- None,
- title="Institute Name",
- description="Institute the user belongs to.",
- json_schema_extra={"example": "PES University"},
- )
diff --git a/app/models/request.py b/app/models/request.py
index c770819..7b03038 100644
--- a/app/models/request.py
+++ b/app/models/request.py
@@ -32,16 +32,6 @@ class RequestModel(BaseModel):
json_schema_extra={"example": True},
)
- know_your_class_and_section: bool = Field(
- False,
- title="Know Your Class and Section Flag",
- description=(
- "Whether to fetch the user's class and section information from the "
- '"Know Your Class and Section" endpoint.'
- ),
- json_schema_extra={"example": True},
- )
-
fields: list[ProfileField] | None = Field(
None,
title="Profile Fields",
diff --git a/app/models/response.py b/app/models/response.py
index f368749..8e8b108 100644
--- a/app/models/response.py
+++ b/app/models/response.py
@@ -5,7 +5,7 @@
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
-from app.models import KYCASModel, ProfileModel
+from app.models import ProfileModel
class ResponseModel(BaseModel):
@@ -39,12 +39,3 @@ class ResponseModel(BaseModel):
title="User Profile Data",
description="The user's profile data returned only if authentication succeeds and profile data was requested.",
)
-
- know_your_class_and_section: KYCASModel | None = Field(
- None,
- title='"Know Your Class and Section" Data',
- description=(
- "The user's class and section data from the "
- '"Know Your Class and Section" endpoint returned only if authentication succeeds.'
- ),
- )
diff --git a/app/pesu.py b/app/pesu.py
index bc70371..5ed0a76 100644
--- a/app/pesu.py
+++ b/app/pesu.py
@@ -12,7 +12,6 @@
from app.exceptions.authentication import (
AuthenticationError,
CSRFTokenError,
- KYCASFetchError,
ProfileFetchError,
ProfileParseError,
)
@@ -29,9 +28,6 @@
"phone",
"campusCode",
"campus",
- "cycle",
- "department",
- "instituteName",
]
@@ -64,18 +60,6 @@ class PESUAcademy:
"Section": "section",
}
- KYCAS_HEADER_TO_KEY_MAP = {
- "PRN": "prn",
- "SRN": "srn",
- "Name": "name",
- "Class": "semester",
- "Section": "section",
- "Cycle": "cycle",
- "Department": "department",
- "Branch": "branch",
- "Institute Name": "instituteName",
- }
-
def __init__(self) -> None:
"""Initialize the PESUAcademy class."""
self._csrf_token: str | None = None
@@ -272,93 +256,11 @@ async def get_profile_information(
return profile
- async def get_know_your_class_and_section(
- self,
- client: httpx.AsyncClient,
- csrf_token: str,
- username: str,
- ) -> dict[str, Any]:
- """Get the class and section information of the user from the "Know Your Class and Section" endpoint.
-
- Args:
- client (httpx.AsyncClient): The authenticated HTTP client to use for making requests.
- csrf_token (str): The authenticated CSRF token.
- username (str): The username of the user, usually their SRN or PRN.
-
- Returns:
- dict[str, Any]: A dictionary containing the user's class and section information.
- """
- logging.info(f'Fetching class and section data for user={username} from "Know Your Class and Section" page...')
- kycas_url = "https://www.pesuacademy.com/Academy/a/getStudentClassInfo"
- kycas_data = {"controllerMode": "370", "actionType": "174", "loginId": username}
- kycas_headers = {
- "origin": "https://www.pesuacademy.com",
- "referer": "https://www.pesuacademy.com/Academy/",
- "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
- "x-csrf-token": csrf_token,
- "x-requested-with": "XMLHttpRequest",
- }
-
- try:
- response = await client.post(kycas_url, data=kycas_data, headers=kycas_headers)
- except Exception:
- raise KYCASFetchError(
- f'Failed to send "Know Your Class and Section" request to PESU Academy for user={username}.',
- )
-
- if response.status_code != 200:
- raise KYCASFetchError(
- f'Failed to fetch "Know Your Class and Section" data from PESU Academy for user={username}. '
- f"Received status code {response.status_code}.",
- )
-
- soup = await asyncio.to_thread(HTMLParser, response.text)
- kycas: dict[str, Any] = {}
-
- table = soup.css_first("table")
- if not table:
- raise KYCASFetchError(
- f'Could not find "Know Your Class and Section" table in the response for user={username}.',
- )
-
- headers = [th.text(strip=True) for th in table.css("thead th")]
- if not headers:
- raise KYCASFetchError(
- f'Could not find "Know Your Class and Section" table headers in the response for user={username}.',
- )
-
- row = table.css_first("tbody tr")
- if not row:
- raise KYCASFetchError(
- f'Could not find "Know Your Class and Section" data row in the response for user={username}.',
- )
-
- cells = [td.text(strip=True) for td in row.css("td")]
-
- if len(headers) != len(cells):
- raise KYCASFetchError(
- f'Mismatch between "Know Your Class and Section" table headers ({len(headers)}) '
- f"and cells ({len(cells)}) for user={username}.",
- )
-
- for header, cell_value in zip(headers, cells):
- if mapped_key := self.KYCAS_HEADER_TO_KEY_MAP.get(header):
- kycas[mapped_key] = cell_value
-
- if not kycas:
- raise KYCASFetchError(
- f'No "Know Your Class and Section" data could be extracted for user={username}.',
- )
-
- logging.info(f'"Know Your Class and Section" data retrieved for user={username}: {kycas}.')
- return kycas
-
async def authenticate(
self,
username: str,
password: str,
profile: bool = False,
- know_your_class_and_section: bool = False,
fields: list[str] | None = None,
) -> dict[str, Any]:
"""Authenticate the user with the provided username and password.
@@ -367,8 +269,6 @@ async def authenticate(
username (str): The username of the user, usually their PRN/email/phone number.
password (str): The password of the user.
profile (bool, optional): Whether to fetch the profile information or not. Defaults to False.
- know_your_class_and_section (bool, optional): Whether to fetch from the
- "Know Your Class and Section" endpoint or not. Defaults to False.
fields (Optional[list[str]], optional): The fields to fetch from the profile.
Defaults to None, which means all default fields will be fetched.
@@ -435,27 +335,6 @@ async def authenticate(
f"Field filtering enabled. Filtered profile data for user={username}: {result['profile']}",
)
- if know_your_class_and_section:
- logging.info(
- f'"Know Your Class and Section" data requested for user={username}. '
- 'Fetching "Know Your Class and Section" data...',
- )
- # Fetch the class and section information
- result["knowYourClassAndSection"] = await self.get_know_your_class_and_section(
- client,
- csrf_token,
- username,
- )
- # Filter the fields if field filtering is enabled
- if field_filtering:
- result["knowYourClassAndSection"] = {
- key: value for key, value in result["knowYourClassAndSection"].items() if key in fields
- }
- logging.info(
- f'Field filtering enabled. Filtered "Know Your Class and Section" data for user={username}: '
- f"{result['knowYourClassAndSection']}",
- )
-
logging.info(f"Authentication process for user={username} completed successfully.")
# Close the client and return the result
diff --git a/scripts/benchmark/benchmark_requests.py b/scripts/benchmark/benchmark_requests.py
index 1a26e25..8098da6 100644
--- a/scripts/benchmark/benchmark_requests.py
+++ b/scripts/benchmark/benchmark_requests.py
@@ -31,12 +31,6 @@
help="Run the authenticate endpoint benchmark without fetching profile information "
"(default: fetch profile info)",
)
- parser.add_argument(
- "--know-your-class-and-section",
- action="store_true",
- help="Run the authenticate endpoint benchmark with class and section information "
- "(default: do not fetch class and section info)",
- )
parser.add_argument(
"--parallel",
action="store_true",
@@ -76,7 +70,6 @@
max_workers = args.max_workers
num_requests = args.num_requests
profile = not args.no_profile
- know_your_class_and_section = args.know_your_class_and_section
parallel = args.parallel
host = args.host
route = args.route
@@ -95,7 +88,6 @@
executor.submit(
make_request,
profile=profile,
- know_your_class_and_section=know_your_class_and_section,
host=host,
route=route,
timeout=timeout,
@@ -119,7 +111,6 @@
for _ in tqdm(range(num_requests), desc="Processing requests"):
response, elapsed = make_request(
profile=profile,
- know_your_class_and_section=know_your_class_and_section,
host=host,
route=route,
timeout=timeout,
diff --git a/scripts/benchmark/util.py b/scripts/benchmark/util.py
index cb2a524..173856d 100644
--- a/scripts/benchmark/util.py
+++ b/scripts/benchmark/util.py
@@ -13,7 +13,6 @@ def make_request(
host: str = "http://localhost:5000",
route: str = "authenticate",
profile: bool = True,
- know_your_class_and_section: bool = False,
timeout: float = 10.0,
) -> tuple[dict, float]:
"""Make a request to the authentication endpoint and return the response and elapsed time.
@@ -22,7 +21,6 @@ def make_request(
host: The host to make the request to
route: The route to make the request to
profile: Whether to fetch the profile information or not
- know_your_class_and_section: Whether to fetch class and section information or not
timeout: The timeout for the request
Returns:
@@ -34,7 +32,6 @@ def make_request(
"username": os.getenv("TEST_PRN"),
"password": os.getenv("TEST_PASSWORD"),
"profile": profile,
- "knowYourClassAndSection": know_your_class_and_section,
}
start_time = time.time()
response = client.post(
diff --git a/tests/functional/test_authenticate_functional.py b/tests/functional/test_authenticate_functional.py
index 6554248..f5ccd07 100644
--- a/tests/functional/test_authenticate_functional.py
+++ b/tests/functional/test_authenticate_functional.py
@@ -166,89 +166,3 @@ async def test_authenticate_invalid_credentials(pesu_academy: PESUAcademy):
assert result["status"] is False
assert "Invalid username or password" in result["message"]
assert "profile" not in result
-
-
-@pytest.mark.secret_required
-@pytest.mark.asyncio
-async def test_authenticate_with_kycas(pesu_academy: PESUAcademy):
- """Test successful authentication with "Know Your Class and Section" data."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- result = await pesu_academy.authenticate(
- email,
- password,
- know_your_class_and_section=True,
- fields=None,
- )
- assert result["status"] is True
- assert "Login successful" in result["message"]
- assert "knowYourClassAndSection" in result
- kycas = result["knowYourClassAndSection"]
- assert "prn" in kycas or "srn" in kycas
- assert "name" in kycas
-
-
-@pytest.mark.secret_required
-@pytest.mark.asyncio
-async def test_authenticate_with_kycas_and_profile(pesu_academy: PESUAcademy):
- """Test authentication requesting both profile and "Know Your Class and Section" data."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- result = await pesu_academy.authenticate(
- email,
- password,
- profile=True,
- know_your_class_and_section=True,
- fields=None,
- )
- assert result["status"] is True
- assert "profile" in result
- assert "knowYourClassAndSection" in result
-
-
-@pytest.mark.secret_required
-@pytest.mark.asyncio
-async def test_authenticate_with_kycas_field_filtering(pesu_academy: PESUAcademy):
- """Test that "Know Your Class and Section" data respects field filtering."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- result = await pesu_academy.authenticate(
- email,
- password,
- know_your_class_and_section=True,
- fields=["name", "semester"],
- )
- assert result["status"] is True
- kycas = result["knowYourClassAndSection"]
- assert "name" in kycas
- assert "semester" in kycas
- assert "prn" not in kycas
- assert "branch" not in kycas
-
-
-@pytest.mark.secret_required
-@pytest.mark.asyncio
-async def test_authenticate_without_kycas(pesu_academy: PESUAcademy):
- """Test that "Know Your Class and Section" data is NOT returned when not requested."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- result = await pesu_academy.authenticate(
- email,
- password,
- know_your_class_and_section=False,
- fields=None,
- )
- assert result["status"] is True
- assert "knowYourClassAndSection" not in result
diff --git a/tests/integration/test_app_integration.py b/tests/integration/test_app_integration.py
index 718c0f1..18556e3 100644
--- a/tests/integration/test_app_integration.py
+++ b/tests/integration/test_app_integration.py
@@ -356,105 +356,6 @@ def test_unhandled_exception_handler(client):
assert data["message"] == "Internal Server Error. Please try again later."
-def test_integration_authenticate_kycas_wrong_type(client):
- """Test that non-boolean knowYourClassAndSection is rejected."""
- payload = {
- "username": "username",
- "password": "password",
- "knowYourClassAndSection": "true",
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 400
- data = response.json()
- assert data["status"] is False
- assert "Could not validate request data" in data["message"]
- assert "body.knowYourClassAndSection: Input should be a valid boolean" in data["message"]
-
-@pytest.mark.secret_required
-def test_integration_authenticate_with_kycas(client):
- """Test successful authentication with "Know Your Class and Section" data."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- payload = {
- "username": email,
- "password": password,
- "knowYourClassAndSection": True,
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 200
- data = response.json()
- assert data["status"] is True
- assert data["message"] == "Login successful."
- assert "knowYourClassAndSection" in data
- kycas = data["knowYourClassAndSection"]
- assert "prn" in kycas or "srn" in kycas or "name" in kycas
-
-
-@pytest.mark.secret_required
-def test_integration_authenticate_with_profile_and_kycas(client):
- """Test successful authentication requesting both profile and "Know Your Class and Section"."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- payload = {
- "username": email,
- "password": password,
- "profile": True,
- "knowYourClassAndSection": True,
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 200
- data = response.json()
- assert data["status"] is True
- assert "profile" in data
- assert "knowYourClassAndSection" in data
-
-
-@pytest.mark.secret_required
-def test_integration_authenticate_kycas_without_requesting(client):
- """Test that "Know Your Class and Section" data is NOT returned when know_your_class_and_section is False."""
- email = os.getenv("TEST_EMAIL")
- password = os.getenv("TEST_PASSWORD")
- assert email is not None, "TEST_EMAIL environment variable not set"
- assert password is not None, "TEST_PASSWORD environment variable not set"
-
- payload = {
- "username": email,
- "password": password,
- "knowYourClassAndSection": False,
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 200
- data = response.json()
- assert data["status"] is True
- assert data.get("knowYourClassAndSection") is None
-
-
-def test_integration_authenticate_deprecated_know_your_class_and_section_key_rejected(client):
- """Test that the old snake_case know_your_class_and_section key is rejected with 400."""
- payload = {
- "username": "username",
- "password": "password",
- "know_your_class_and_section": True,
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 400
- data = response.json()
- assert data["status"] is False
- assert "Could not validate request data" in data["message"]
- assert "body.know_your_class_and_section: Extra inputs are not permitted" in data["message"]
-
-
def test_integration_authenticate_unknown_extra_key_rejected(client):
"""Test that any unknown key in the request body is rejected with 400."""
payload = {
@@ -486,20 +387,3 @@ def test_integration_authenticate_deprecated_campus_code_in_fields_rejected(clie
assert data["status"] is False
assert "Could not validate request data" in data["message"]
assert "body.fields.0" in data["message"]
-
-
-def test_integration_authenticate_deprecated_institute_name_in_fields_rejected(client):
- """Test that the old snake_case institute_name is rejected as a fields value."""
- payload = {
- "username": "username",
- "password": "password",
- "profile": True,
- "fields": ["institute_name"],
- }
-
- response = client.post("/authenticate", json=payload)
- assert response.status_code == 400
- data = response.json()
- assert data["status"] is False
- assert "Could not validate request data" in data["message"]
- assert "body.fields.0" in data["message"]
diff --git a/tests/unit/test_pesu.py b/tests/unit/test_pesu.py
index cff8c6d..7df74e1 100644
--- a/tests/unit/test_pesu.py
+++ b/tests/unit/test_pesu.py
@@ -5,7 +5,6 @@
from app.exceptions.authentication import (
AuthenticationError,
CSRFTokenError,
- KYCASFetchError,
ProfileFetchError,
ProfileParseError,
)
@@ -173,7 +172,7 @@ async def test_authenticate_login_form_present(mock_get, mock_post, mock_html_pa
MagicMock(attributes={"content": "fake-csrf-token"}) if selector == "meta[name='csrf-token']" else None
)
mock_soup_login = MagicMock()
- mock_soup_login.css_first.side_effect = lambda selector: (MagicMock() if selector == "div.login-form" else None)
+ mock_soup_login.css_first.side_effect = lambda selector: MagicMock() if selector == "div.login-form" else None
mock_html_parser.side_effect = [mock_soup_csrf, mock_soup_login]
mock_post_response = MagicMock()
mock_post_response.text = "
"
@@ -335,8 +334,8 @@ def css_first(selector):
mock_soup_rr = MagicMock()
mock_container_rr = MagicMock()
mock_container_rr.css.return_value = form_group_elems_rr
- mock_soup_rr.css_first.side_effect = (
- lambda selector: mock_container_rr if selector == "div.elem-info-wrapper" else None
+ mock_soup_rr.css_first.side_effect = lambda selector: (
+ mock_container_rr if selector == "div.elem-info-wrapper" else None
)
mock_html_parser.return_value = mock_soup_rr
@@ -360,8 +359,8 @@ def css_first(selector):
mock_soup_ec = MagicMock()
mock_container_ec = MagicMock()
mock_container_ec.css.return_value = form_group_elems_ec
- mock_soup_ec.css_first.side_effect = (
- lambda selector: mock_container_ec if selector == "div.elem-info-wrapper" else None
+ mock_soup_ec.css_first.side_effect = lambda selector: (
+ mock_container_ec if selector == "div.elem-info-wrapper" else None
)
mock_html_parser.return_value = mock_soup_ec
@@ -389,11 +388,10 @@ async def test_get_profile_information_no_profile_data(mock_get, mock_html_parse
client.get.return_value = mock_response
with pytest.raises(ProfileParseError) as exc_info:
await pesu.get_profile_information(client, "testuser")
- assert "Failed to parse student profile page from PESU Academy for user=testuser." in str(exc_info.value)
+ assert "Failed to parse student profile page from PESU Academy for user=testuser." in str(exc_info.value)
assert "The webpage might have changed." in str(exc_info.value)
-
@patch("app.pesu.HTMLParser")
@patch("app.pesu.httpx.AsyncClient.get")
@patch("app.pesu.PESUAcademy._extract_and_update_profile", new_callable=MagicMock)
@@ -491,403 +489,6 @@ def test_default_fields_is_list():
assert "campusCode" in PESUAcademy.DEFAULT_FIELDS
assert "campus" in PESUAcademy.DEFAULT_FIELDS
-@pytest.mark.asyncio
-async def test_get_kycas_http_exception(pesu):
- """Test that the "Know Your Class and Section" fetch error is raised on request failure."""
- client = AsyncMock()
- client.post.side_effect = Exception("Connection error")
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'Failed to send "Know Your Class and Section" request' in str(exc_info.value)
-
-
-@pytest.mark.asyncio
-async def test_get_kycas_non_200_status(pesu):
- """Test that the "Know Your Class and Section" fetch error is raised on non-200 responses."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 500
- client.post.return_value = mock_response
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert "Received status code 500" in str(exc_info.value)
-
-
-@patch("app.pesu.HTMLParser")
-@pytest.mark.asyncio
-async def test_get_kycas_no_table(mock_html_parser, pesu):
- """Test that the "Know Your Class and Section" fetch error is raised when no table is found."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = ""
- client.post.return_value = mock_response
-
- mock_soup = MagicMock()
- mock_soup.css_first.return_value = None
- mock_html_parser.return_value = mock_soup
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'Could not find "Know Your Class and Section" table' in str(exc_info.value)
-
-
-@patch("app.pesu.HTMLParser")
-@pytest.mark.asyncio
-async def test_get_kycas_no_headers(mock_html_parser, pesu):
- """Test that the "Know Your Class and Section" fetch error is raised when headers are empty."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = ""
- client.post.return_value = mock_response
-
- mock_table = MagicMock()
- mock_table.css.return_value = []
-
- mock_soup = MagicMock()
- mock_soup.css_first.return_value = mock_table
- mock_html_parser.return_value = mock_soup
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'Could not find "Know Your Class and Section" table headers' in str(exc_info.value)
-
-
-@patch("app.pesu.HTMLParser")
-@pytest.mark.asyncio
-async def test_get_kycas_no_data_row(mock_html_parser, pesu):
- """Test that the "Know Your Class and Section" fetch error is raised when no row exists."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = ""
- client.post.return_value = mock_response
-
- mock_th = MagicMock()
- mock_th.text.return_value = "PRN"
-
- mock_table = MagicMock()
- mock_table.css.return_value = [mock_th]
- mock_table.css_first.return_value = None
-
- mock_soup = MagicMock()
- mock_soup.css_first.return_value = mock_table
- mock_html_parser.return_value = mock_soup
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'Could not find "Know Your Class and Section" data row' in str(exc_info.value)
-
-
-@patch("app.pesu.HTMLParser")
-@pytest.mark.asyncio
-async def test_get_kycas_header_cell_mismatch(mock_html_parser, pesu):
- """Test that the "Know Your Class and Section" fetch error is raised on malformed rows."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = ""
- client.post.return_value = mock_response
-
- mock_th1 = MagicMock()
- mock_th1.text.return_value = "PRN"
- mock_th2 = MagicMock()
- mock_th2.text.return_value = "SRN"
-
- mock_td1 = MagicMock()
- mock_td1.text.return_value = "PES1201800001"
-
- mock_row = MagicMock()
- mock_row.css.return_value = [mock_td1]
-
- mock_table = MagicMock()
- mock_table.css.return_value = [mock_th1, mock_th2]
- mock_table.css_first.return_value = mock_row
-
- mock_soup = MagicMock()
- mock_soup.css_first.return_value = mock_table
- mock_html_parser.return_value = mock_soup
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'Mismatch between "Know Your Class and Section" table headers' in str(exc_info.value)
-
-
-@patch("app.pesu.HTMLParser")
-@pytest.mark.asyncio
-async def test_get_kycas_no_mapped_keys(mock_html_parser, pesu):
- """Test that the "Know Your Class and Section" fetch error is raised on unknown headers."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = ""
- client.post.return_value = mock_response
-
- mock_th = MagicMock()
- mock_th.text.return_value = "UnknownHeader"
-
- mock_td = MagicMock()
- mock_td.text.return_value = "some_value"
-
- mock_row = MagicMock()
- mock_row.css.return_value = [mock_td]
-
- mock_table = MagicMock()
- mock_table.css.return_value = [mock_th]
- mock_table.css_first.return_value = mock_row
-
- mock_soup = MagicMock()
- mock_soup.css_first.return_value = mock_table
- mock_html_parser.return_value = mock_soup
-
- with pytest.raises(KYCASFetchError) as exc_info:
- await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
- assert 'No "Know Your Class and Section" data could be extracted' in str(exc_info.value)
-
-
-@pytest.mark.asyncio
-async def test_get_kycas_success(pesu):
- """Test the happy path: successfully parsing "Know Your Class and Section" data."""
- client = AsyncMock()
- mock_response = MagicMock()
- mock_response.status_code = 200
- mock_response.text = """
-
-
-
- | PRN |
- SRN |
- Name |
- Class |
- Section |
- Cycle |
- Department |
- Branch |
- Institute Name |
-
-
-
-
- | PES2202100984 |
- PES2UG21CS310 |
- Test User |
- Sem-8 |
- Section F |
- NA |
- CSE(EC Campus) |
- CSE |
- PES University (Electronic City) |
-
-
-
- """
- client.post.return_value = mock_response
-
- result = await pesu.get_know_your_class_and_section(client, "fake-csrf", "testuser")
-
- assert result["prn"] == "PES2202100984"
- assert result["srn"] == "PES2UG21CS310"
- assert result["name"] == "Test User"
- assert result["semester"] == "Sem-8"
- assert result["section"] == "Section F"
- assert result["cycle"] == "NA"
- assert result["department"] == "CSE(EC Campus)"
- assert result["branch"] == "CSE"
- assert result["instituteName"] == "PES University (Electronic City)"
-
-
-@patch("app.pesu.httpx.AsyncClient.get")
-@patch("app.pesu.httpx.AsyncClient.post")
-@patch("app.pesu.PESUAcademy.get_know_your_class_and_section")
-@pytest.mark.asyncio
-async def test_authenticate_passes_kycas_flag(mock_get_kycas, mock_post, mock_get, pesu):
- """Test that authenticate calls get_know_your_class_and_section when the flag is set."""
- mock_get_response = AsyncMock()
- mock_get_response.text = ''
- mock_get.return_value = mock_get_response
-
- mock_post_response = AsyncMock()
- mock_post_response.text = ''
- mock_post.return_value = mock_post_response
-
- mock_get_kycas.return_value = {
- "prn": "PES1201800001",
- "srn": "PES1UG19CS001",
- "name": "John Doe",
- "semester": "Sem-6",
- "section": "Section A",
- "cycle": "NA",
- "department": "CSE(RR Campus)",
- "branch": "CSE",
- "instituteName": "PES University",
- }
-
- result = await pesu.authenticate("testuser", "testpass", profile=False, know_your_class_and_section=True)
-
- assert result["status"] is True
- assert result["knowYourClassAndSection"]["semester"] == "Sem-6"
- mock_get_kycas.assert_called_once()
-
-
-@patch("app.pesu.httpx.AsyncClient.get")
-@patch("app.pesu.httpx.AsyncClient.post")
-@pytest.mark.asyncio
-async def test_authenticate_success_no_kycas(mock_post, mock_get, pesu):
- """Test that "Know Your Class and Section" data is NOT returned when not requested."""
- mock_get_response = AsyncMock()
- mock_get_response.text = ''
- mock_get.return_value = mock_get_response
-
- mock_post_response = AsyncMock()
- mock_post_response.text = ''
- mock_post.return_value = mock_post_response
-
- result = await pesu.authenticate("user", "pass", know_your_class_and_section=False)
- assert result["status"] is True
- assert "knowYourClassAndSection" not in result
-
-
-@patch("app.pesu.httpx.AsyncClient.get")
-@patch("app.pesu.httpx.AsyncClient.post")
-@patch("app.pesu.PESUAcademy.get_know_your_class_and_section")
-@pytest.mark.asyncio
-async def test_authenticate_with_kycas(mock_get_kycas, mock_post, mock_get, pesu):
- """Test that "Know Your Class and Section" data is returned when requested."""
- mock_get_response = AsyncMock()
- mock_get_response.text = ''
- mock_get.return_value = mock_get_response
-
- mock_post_response = AsyncMock()
- mock_post_response.text = ''
- mock_post.return_value = mock_post_response
-
- mock_get_kycas.return_value = {
- "prn": "PES1201800001",
- "srn": "PES1UG19CS001",
- "name": "John Doe",
- "semester": "Sem-6",
- "section": "Section A",
- "cycle": "NA",
- "department": "CSE(RR Campus)",
- "branch": "CSE",
- "instituteName": "PES University",
- }
-
- result = await pesu.authenticate("user", "pass", know_your_class_and_section=True)
-
- assert result["status"] is True
- assert "knowYourClassAndSection" in result
- assert result["knowYourClassAndSection"]["prn"] == "PES1201800001"
- assert result["knowYourClassAndSection"]["instituteName"] == "PES University"
-
-
-@patch("app.pesu.httpx.AsyncClient.get")
-@patch("app.pesu.httpx.AsyncClient.post")
-@patch("app.pesu.PESUAcademy.get_know_your_class_and_section")
-@pytest.mark.asyncio
-async def test_authenticate_with_kycas_field_filtering(mock_get_kycas, mock_post, mock_get, pesu):
- """Test that "Know Your Class and Section" data is filtered when field filtering is enabled."""
- mock_get_response = AsyncMock()
- mock_get_response.text = ''
- mock_get.return_value = mock_get_response
-
- mock_post_response = AsyncMock()
- mock_post_response.text = ''
- mock_post.return_value = mock_post_response
-
- mock_get_kycas.return_value = {
- "prn": "PES1201800001",
- "srn": "PES1UG19CS001",
- "name": "John Doe",
- "semester": "Sem-6",
- "section": "Section A",
- "cycle": "NA",
- "department": "CSE(RR Campus)",
- "branch": "CSE",
- "instituteName": "PES University",
- }
-
- result = await pesu.authenticate(
- "user",
- "pass",
- know_your_class_and_section=True,
- fields=["name", "semester"],
- )
-
- assert result["status"] is True
- kycas = result["knowYourClassAndSection"]
- assert "name" in kycas
- assert "semester" in kycas
- assert "prn" not in kycas
- assert "branch" not in kycas
- assert "instituteName" not in kycas
-
-
-@patch("app.pesu.httpx.AsyncClient.get")
-@patch("app.pesu.httpx.AsyncClient.post")
-@patch("app.pesu.PESUAcademy.get_profile_information")
-@patch("app.pesu.PESUAcademy.get_know_your_class_and_section")
-@pytest.mark.asyncio
-async def test_authenticate_with_both_profile_and_kycas(
- mock_get_kycas, mock_get_profile, mock_post, mock_get, pesu
-):
- """Test requesting both profile and "Know Your Class and Section" data simultaneously."""
- mock_get_response = AsyncMock()
- mock_get_response.text = ''
- mock_get.return_value = mock_get_response
-
- mock_post_response = AsyncMock()
- mock_post_response.text = ''
- mock_post.return_value = mock_post_response
-
- mock_get_profile.return_value = {
- "name": "John Doe",
- "prn": "PES1201800001",
- "email": "john@example.com",
- }
- mock_get_kycas.return_value = {
- "prn": "PES1201800001",
- "semester": "Sem-6",
- "section": "Section A",
- }
-
- result = await pesu.authenticate(
- "user", "pass", profile=True, know_your_class_and_section=True
- )
-
- assert result["status"] is True
- assert "profile" in result
- assert "knowYourClassAndSection" in result
- assert result["profile"]["name"] == "John Doe"
- assert result["knowYourClassAndSection"]["semester"] == "Sem-6"
-
-def test_kycas_header_to_key_map_is_dict():
- """Test that the "Know Your Class and Section" header map has expected keys."""
- kmap = PESUAcademy.KYCAS_HEADER_TO_KEY_MAP
- assert isinstance(kmap, dict)
- assert "PRN" in kmap
- assert "SRN" in kmap
- assert "Name" in kmap
- assert "Class" in kmap
- assert kmap["Class"] == "semester"
- assert "Section" in kmap
- assert "Cycle" in kmap
- assert "Department" in kmap
- assert "Branch" in kmap
- assert "Institute Name" in kmap
-
-
-def test_default_fields_includes_kycas_relevant_fields():
- """Test that DEFAULT_FIELDS includes fields relevant to "Know Your Class and Section" filtering."""
- fields = PESUAcademy.DEFAULT_FIELDS
- assert "semester" in fields
- assert "cycle" in fields
- assert "department" in fields
- assert "instituteName" in fields
-
@pytest.mark.asyncio
@patch("app.pesu.PESUAcademy._fetch_new_client_with_csrf_token")
diff --git a/tests/unit/test_request_model.py b/tests/unit/test_request_model.py
index adde8b7..cbc41fb 100644
--- a/tests/unit/test_request_model.py
+++ b/tests/unit/test_request_model.py
@@ -112,66 +112,16 @@ def test_validate_password_strips_whitespace():
assert model.password == "testpass"
-def test_validate_know_your_class_and_section_default_false():
- """Test that know_your_class_and_section defaults to False."""
- model = RequestModel(username="testuser", password="testpass")
- assert model.know_your_class_and_section is False
-
-
-def test_validate_know_your_class_and_section_true():
- """Test setting knowYourClassAndSection to True via camelCase alias."""
- model = RequestModel.model_validate({
- "username": "testuser",
- "password": "testpass",
- "knowYourClassAndSection": True,
- })
- assert model.know_your_class_and_section is True
-
-
-def test_validate_know_your_class_and_section_invalid_type():
- """Test that non-boolean types are rejected for knowYourClassAndSection."""
- with pytest.raises(ValidationError) as exc_info:
- RequestModel.model_validate({
- "username": "testuser",
- "password": "testpass",
- "knowYourClassAndSection": "yes",
- })
- assert exc_info.value.errors()[0]["type"] == "bool_type"
- assert "Input should be a valid boolean" in str(exc_info.value)
-
-
-def test_validate_know_your_class_and_section_int_rejected():
- """Test that integer types are rejected (strict mode)."""
- with pytest.raises(ValidationError) as exc_info:
- RequestModel.model_validate({
- "username": "testuser",
- "password": "testpass",
- "knowYourClassAndSection": 1,
- })
- assert "Input should be a valid boolean" in str(exc_info.value)
-
-
-def test_validate_deprecated_know_your_class_and_section_key_rejected():
- """Test that the old snake_case know_your_class_and_section key is rejected as an extra field."""
- with pytest.raises(ValidationError) as exc_info:
- RequestModel.model_validate({
- "username": "testuser",
- "password": "testpass",
- "know_your_class_and_section": True,
- })
- errors = exc_info.value.errors()
- assert any(e["type"] == "extra_forbidden" for e in errors)
- assert "Extra inputs are not permitted" in str(exc_info.value)
-
-
def test_validate_unknown_extra_key_rejected():
"""Test that any unknown key is rejected."""
with pytest.raises(ValidationError) as exc_info:
- RequestModel.model_validate({
- "username": "testuser",
- "password": "testpass",
- "someRandomField": "value",
- })
+ RequestModel.model_validate(
+ {
+ "username": "testuser",
+ "password": "testpass",
+ "someRandomField": "value",
+ }
+ )
errors = exc_info.value.errors()
assert any(e["type"] == "extra_forbidden" for e in errors)
assert "Extra inputs are not permitted" in str(exc_info.value)
@@ -184,12 +134,3 @@ def test_validate_deprecated_campus_code_in_fields_rejected():
errors = exc_info.value.errors()
assert any(e["type"] == "literal_error" for e in errors)
assert "fields.0" in str(exc_info.value)
-
-
-def test_validate_deprecated_institute_name_in_fields_rejected():
- """Test that the old snake_case institute_name is rejected as a fields value."""
- with pytest.raises(ValidationError) as exc_info:
- RequestModel(username="testuser", password="testpass", fields=["institute_name"])
- errors = exc_info.value.errors()
- assert any(e["type"] == "literal_error" for e in errors)
- assert "fields.0" in str(exc_info.value)