diff --git a/.gitignore b/.gitignore index ea8c4bf..3948166 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +api_responses_*.txt diff --git a/OPENAPI.md b/OPENAPI.md new file mode 100644 index 0000000..dc127ee --- /dev/null +++ b/OPENAPI.md @@ -0,0 +1,108 @@ +# OpenAPI Documentation + +This directory contains the OpenAPI 3.0 specification for the Home Access Center API. + +## Files + +- **openapi.yaml** - OpenAPI specification in YAML format (recommended for editing) +- **openapi.json** - OpenAPI specification in JSON format (auto-generated from YAML) + +## Viewing the Documentation + +### Interactive Documentation (Recommended) + +Visit the hosted Swagger UI interface: +``` +https://hac.packjack.dev/docs +``` + +This provides: +- Interactive API explorer with "Try it out" functionality +- Complete parameter descriptions and examples +- Response schemas and examples +- No downloads or setup required +- Search and filter capabilities + +### Raw Specification Files + +The OpenAPI specification files are also available: +- YAML: https://hac.packjack.dev/openapi.yaml +- JSON: https://hac.packjack.dev/openapi.json + +### Using with API Tools + +You can use these specification files with various OpenAPI tools: + +#### Swagger Editor +Visit https://editor.swagger.io/ and import the URL: +``` +https://hac.packjack.dev/openapi.yaml +``` + +#### Redoc +Visit https://redocly.github.io/redoc/ and import the URL: +``` +https://hac.packjack.dev/openapi.yaml +``` + +#### Postman +1. Open Postman +2. Click "Import" +3. Select "Link" tab +4. Paste: `https://hac.packjack.dev/openapi.json` +5. Click "Import" + +#### curl (download locally) +```bash +curl -O https://hac.packjack.dev/openapi.yaml +curl -O https://hac.packjack.dev/openapi.json +``` + +## Updating the Documentation + +If you modify `openapi.yaml`, regenerate the JSON file: + +```bash +npm install -g js-yaml +npx js-yaml openapi.yaml > openapi.json +``` + +To validate the OpenAPI spec: + +```bash +npm install -g @apidevtools/swagger-cli +swagger-cli validate openapi.yaml +``` + +## API Overview + +The Home Access Center API provides 11 endpoints: + +### Student Information +- `GET /api/name` - Get student name +- `GET /api/info` - Get student profile information + +### Classes +- `GET /api/classes` - Get list of classes +- `GET /api/averages` - Get class averages +- `GET /api/weightings` - Get grade category weightings + +### Assignments & Grades +- `GET /api/assignments` - Get detailed assignments +- `GET /api/gradebook` - Get complete gradebook with grades and weightings + +### Reports +- `GET /api/reportcard` - Get report card +- `GET /api/ipr` - Get interim progress report +- `GET /api/transcript` - Get full transcript with GPA +- `GET /api/rank` - Get GPA rank and quartile + +All endpoints (except `/` and `/api/`) require authentication via query parameters: +- `user` - Home Access Center username +- `pass` - Home Access Center password + +Optional parameters: +- `link` - Home Access Center base URL (defaults to https://homeaccess.katyisd.org) +- `short` - Return shortened class names (boolean) +- `six_weeks` - Specific six weeks period (for assignment endpoints) +- `no_cache` - Bypass cache and fetch fresh data (boolean) diff --git a/TEST_SCRIPT.md b/TEST_SCRIPT.md new file mode 100644 index 0000000..b1ff0c5 --- /dev/null +++ b/TEST_SCRIPT.md @@ -0,0 +1,179 @@ +# API Endpoint Testing Script + +This bash script (`test_endpoints.sh`) automatically calls all Home Access Center API endpoints and saves the responses to a text file. + +## Features + +- Tests all 11 authenticated API endpoints +- Tests both regular and short-name variants where applicable +- Saves responses to a timestamped text file +- Shows color-coded success/failure status +- Includes HTTP status codes +- Supports custom HAC base URLs +- 30-second timeout per request + +## Requirements + +- `bash` shell +- `curl` command-line tool +- Access to the API server (locally or remote) +- Valid Home Access Center credentials + +## Usage + +### Basic Usage + +```bash +./test_endpoints.sh +``` + +This will: +- Connect to `http://localhost:3000` (default API server) +- Use `https://homeaccess.katyisd.org` (default HAC URL) +- Save results to `api_responses_YYYYMMDD_HHMMSS.txt` + +### Custom HAC URL + +```bash +./test_endpoints.sh https://homeaccess.yourschool.org +``` + +### Custom API Server + +```bash +./test_endpoints.sh https://homeaccess.katyisd.org http://localhost:3000 +``` + +### Remote API Server + +```bash +./test_endpoints.sh https://homeaccess.katyisd.org https://hac.packjack.dev +``` + +## Examples + +### Local Development + +```bash +# Start the API server +cargo run + +# In another terminal, run the test script +./test_endpoints.sh student123 mypassword +``` + +### Testing Production + +```bash +./test_endpoints.sh student123 mypassword https://homeaccess.katyisd.org https://hac.packjack.dev +``` + +## Endpoints Tested + +The script tests the following endpoints: + +1. `/` - Root endpoint (no auth) +2. `/api/name` - Student name +3. `/api/info` - Student information +4. `/api/classes` - List of classes (regular and short) +5. `/api/averages` - Class averages (regular and short) +6. `/api/assignments` - Detailed assignments (regular and short) +7. `/api/gradebook` - Complete gradebook (regular and short) +8. `/api/weightings` - Grade weightings (regular and short) +9. `/api/reportcard` - Report card +10. `/api/ipr` - Interim progress report +11. `/api/transcript` - Full transcript +12. `/api/rank` - GPA rank and quartile + +## Output Format + +The script creates a timestamped file (e.g., `api_responses_20251104_143022.txt`) with: + +``` +======================================================================== + HOME ACCESS CENTER API - ENDPOINT TEST RESULTS +======================================================================== + +Test Date: 2025-11-04 14:30:22 +API URL: http://localhost:3000 +HAC Base URL: https://homeaccess.katyisd.org +Username: student123 + +This file contains responses from all API endpoints. + +================================================================================ +ENDPOINT: /api/name +DESCRIPTION: Get student name +URL: http://localhost:3000/api/name?user=student123&pass=... +TIMESTAMP: 2025-11-04 14:30:25 +HTTP STATUS: 200 +================================================================================ + +{"name": "John Doe"} + +... +``` + +## Viewing Results + +```bash +# View the entire file +cat api_responses_20251104_143022.txt + +# View with pagination +less api_responses_20251104_143022.txt + +# Search for a specific endpoint +grep -A 20 "ENDPOINT: /api/name" api_responses_20251104_143022.txt + +# View only successful responses (HTTP 200) +grep -B 5 "HTTP STATUS: 200" api_responses_20251104_143022.txt +``` + +## Troubleshooting + +### Connection Refused + +If you get connection errors: +1. Make sure the API server is running +2. Check if the API URL is correct +3. Verify the port is accessible + +### Authentication Errors (HTTP 401) + +If you get unauthorized errors: +1. Verify your username and password are correct +2. Check that the HAC base URL is correct for your school district +3. Ensure your account has access to Home Access Center + +### Timeout Errors + +If requests timeout: +1. The HAC server might be slow - this is normal +2. Your credentials might be invalid (causes slow rejection) +3. Network connectivity issues + +## Security Notes + +⚠️ **Important**: This script passes credentials as command-line arguments and in URLs. + +- Don't run this script on shared systems +- Don't commit the output files to version control (they contain your data) +- The output files are added to `.gitignore` automatically +- Consider deleting output files after reviewing them + +## Customization + +You can modify the script to: +- Add more endpoints +- Change the output format +- Add JSON formatting with `jq` +- Filter specific fields from responses +- Add retry logic for failed requests + +Example with `jq` formatting: + +```bash +# Pretty-print JSON responses +cat api_responses_20251104_143022.txt | grep -v "^===" | grep -v "^ENDPOINT" | grep -v "^DESCRIPTION" | grep -v "^URL" | grep -v "^TIMESTAMP" | grep -v "^HTTP STATUS" | jq '.' 2>/dev/null +``` diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..68cde10 --- /dev/null +++ b/openapi.json @@ -0,0 +1,1108 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Home Access Center API", + "description": "A REST API for accessing Home Access Center student data. This API provides programmatic access to student information including grades, assignments, classes, transcripts, and more.\n\n## Authentication\n\nAll endpoints (except the root endpoint) require authentication via query parameters:\n- `user`: Your Home Access Center username\n- `pass`: Your Home Access Center password\n\n## Caching\n\nThe API implements intelligent caching:\n- Login sessions are cached for 30 minutes\n- Page data is cached for 5 minutes\n- Add `?no_cache=true` to any endpoint to bypass cache\n\n## Base URL\n\nThis API is hosted at: `https://hac.packjack.dev`\n", + "version": "0.1.0", + "contact": { + "name": "Home Access Center API" + }, + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "https://hac.packjack.dev", + "description": "Production server" + } + ], + "tags": [ + { + "name": "Student Info", + "description": "Basic student information" + }, + { + "name": "Classes", + "description": "Class-related information" + }, + { + "name": "Assignments", + "description": "Assignment and grade data" + }, + { + "name": "Reports", + "description": "Report cards and transcripts" + } + ], + "paths": { + "/": { + "get": { + "summary": "API Information", + "description": "Returns welcome message and available routes", + "tags": [ + "Student Info" + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "example": "Welcome to the Home Access Center API!" + }, + "message": { + "type": "string", + "example": "Interactive API documentation available at /docs" + }, + "docs_url": { + "type": "string", + "example": "https://hac.packjack.dev/docs" + }, + "openapi_spec": { + "type": "object", + "properties": { + "yaml": { + "type": "string", + "example": "https://hac.packjack.dev/openapi.yaml" + }, + "json": { + "type": "string", + "example": "https://hac.packjack.dev/openapi.json" + } + } + }, + "routes": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "/api/name", + "/api/info", + "/api/classes" + ] + }, + "cache_param": { + "type": "string", + "example": "Add ?no_cache=true to any endpoint to bypass cache" + } + } + } + } + } + } + } + } + }, + "/api/": { + "get": { + "summary": "API Information (alternate route)", + "description": "Returns welcome message and available routes", + "tags": [ + "Student Info" + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WelcomeMessage" + } + } + } + } + } + } + }, + "/docs": { + "get": { + "summary": "Interactive API Documentation", + "description": "Serves an interactive Swagger UI interface for exploring and testing the API", + "tags": [ + "Student Info" + ], + "responses": { + "200": { + "description": "HTML page with Swagger UI", + "content": { + "text/html": { + "schema": { + "type": "string", + "example": "..." + } + } + } + } + } + } + }, + "/openapi.yaml": { + "get": { + "summary": "OpenAPI Specification (YAML)", + "description": "Returns the OpenAPI 3.0 specification in YAML format", + "tags": [ + "Student Info" + ], + "responses": { + "200": { + "description": "OpenAPI specification in YAML format", + "content": { + "application/yaml": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/openapi.json": { + "get": { + "summary": "OpenAPI Specification (JSON)", + "description": "Returns the OpenAPI 3.0 specification in JSON format", + "tags": [ + "Student Info" + ], + "responses": { + "200": { + "description": "OpenAPI specification in JSON format", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/api/name": { + "get": { + "summary": "Get Student Name", + "description": "Retrieves the student's name from Home Access Center", + "tags": [ + "Student Info" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "John Doe" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/info": { + "get": { + "summary": "Get Student Information", + "description": "Retrieves detailed student profile information", + "tags": [ + "Student Info" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cohort_year": { + "type": "string", + "example": "2027" + }, + "counselor": { + "type": "string", + "example": "Jones, Natalie" + }, + "dob": { + "type": "string", + "example": "5/2/2009" + }, + "grade": { + "type": "string", + "example": "11" + }, + "language": { + "type": "string", + "example": "English" + }, + "name": { + "type": "string", + "example": "MacGregor, Jackson North" + }, + "school": { + "type": "string", + "example": "Seven Lakes HS" + } + } + }, + "example": { + "cohort_year": "2027", + "counselor": "Jones, Natalie", + "dob": "5/2/2009", + "grade": "11", + "language": "English", + "name": "MacGregor, Jackson North", + "school": "Seven Lakes HS" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/classes": { + "get": { + "summary": "Get Class List", + "description": "Retrieves a list of the student's classes", + "tags": [ + "Classes" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/Short" + }, + { + "$ref": "#/components/parameters/SixWeeks" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "AP Calculus BC", + "English III Honors", + "AP Chemistry" + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/averages": { + "get": { + "summary": "Get Class Averages", + "description": "Retrieves current averages for all classes", + "tags": [ + "Classes" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/Short" + }, + { + "$ref": "#/components/parameters/SixWeeks" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "AP Calculus BC": "95", + "English III Honors": "92", + "AP Chemistry": "88" + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/assignments": { + "get": { + "summary": "Get Assignments", + "description": "Retrieves detailed assignment information for all classes", + "tags": [ + "Assignments" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/Short" + }, + { + "$ref": "#/components/parameters/SixWeeks" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "AP Calculus BC": [ + [ + "10/15/2024", + "Quiz 3", + "Major Grades", + "95", + "100" + ], + [ + "10/10/2024", + "Homework 5", + "Daily Grades", + "10", + "10" + ] + ] + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/gradebook": { + "get": { + "summary": "Get Complete Gradebook", + "description": "Retrieves assignments with grades and weightings for all classes", + "tags": [ + "Assignments" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/Short" + }, + { + "$ref": "#/components/parameters/SixWeeks" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "assignments": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "weightings": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/weightings": { + "get": { + "summary": "Get Grade Weightings", + "description": "Retrieves grade category weightings for all classes", + "tags": [ + "Classes" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/Short" + }, + { + "$ref": "#/components/parameters/SixWeeks" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array containing: [category, points_earned, total_points, percentage, weight, weighted_value]" + } + } + }, + "example": { + "AP PHYSICS 1 GT": [ + [ + "Major", + "187.0000", + "200.00", + "93.500%", + "70.00", + "65.450000" + ], + [ + "Minor", + "658.0000", + "800.00", + "82.250%", + "20.00", + "16.450000" + ], + [ + "Other", + "197.0000", + "400.00", + "49.250%", + "10.00", + "4.925000" + ] + ], + "AP CALCULUS BC": [ + [ + "Major", + "162.0000", + "200.00", + "81.000%", + "70.00", + "56.700000" + ], + [ + "Minor", + "348.0000", + "400.00", + "87.000%", + "20.00", + "17.400000" + ], + [ + "Other", + "1397.0000", + "1400.00", + "99.785%", + "10.00", + "9.978600" + ] + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/reportcard": { + "get": { + "summary": "Get Report Card", + "description": "Retrieves report card tables with grades per marking period", + "tags": [ + "Reports" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "example": [ + [ + "Course", + "Teacher", + "MP1", + "MP2", + "Sem1", + "MP3", + "MP4", + "Sem2", + "Final" + ], + [ + "AP Calculus BC", + "Smith, John", + "95", + "93", + "94", + "96", + "94", + "95", + "95" + ] + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/ipr": { + "get": { + "summary": "Get Interim Progress Report", + "description": "Retrieves interim progress report data", + "tags": [ + "Reports" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "example": [ + [ + "Course", + "Teacher", + "Grade", + "Absences", + "Tardies" + ], + [ + "AP Calculus BC", + "Smith, John", + "95", + "0", + "0" + ] + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/transcript": { + "get": { + "summary": "Get Full Transcript", + "description": "Retrieves complete transcript with GPA and semester information", + "tags": [ + "Reports" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rank": { + "type": "string", + "example": "15 of 450" + }, + "quartile": { + "type": "string", + "example": "Top 25%" + }, + "Weighted GPA": { + "type": "string", + "example": "4.35" + }, + "Unweighted GPA": { + "type": "string", + "example": "3.95" + } + }, + "additionalProperties": { + "type": "object", + "properties": { + "year": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "credits": { + "type": "string" + }, + "data": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + }, + "/api/rank": { + "get": { + "summary": "Get GPA Rank", + "description": "Retrieves GPA rank and quartile information", + "tags": [ + "Reports" + ], + "parameters": [ + { + "$ref": "#/components/parameters/Username" + }, + { + "$ref": "#/components/parameters/Password" + }, + { + "$ref": "#/components/parameters/Link" + }, + { + "$ref": "#/components/parameters/NoCache" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rank": { + "type": "string", + "example": "15 of 450" + }, + "quartile": { + "type": "string", + "example": "Top 25%" + }, + "Weighted GPA": { + "type": "string", + "example": "4.35" + }, + "Unweighted GPA": { + "type": "string", + "example": "3.95" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + } + } + } + }, + "components": { + "parameters": { + "Username": { + "name": "user", + "in": "query", + "required": true, + "description": "Home Access Center username", + "schema": { + "type": "string" + }, + "example": "student123" + }, + "Password": { + "name": "pass", + "in": "query", + "required": true, + "description": "Home Access Center password", + "schema": { + "type": "string", + "format": "password" + }, + "example": "mypassword" + }, + "Link": { + "name": "link", + "in": "query", + "required": false, + "description": "Home Access Center base URL (defaults to https://homeaccess.katyisd.org)", + "schema": { + "type": "string", + "format": "uri", + "default": "https://homeaccess.katyisd.org" + }, + "example": "https://homeaccess.katyisd.org" + }, + "Short": { + "name": "short", + "in": "query", + "required": false, + "description": "Whether to return shortened class names", + "schema": { + "type": "boolean", + "default": false + }, + "example": true + }, + "SixWeeks": { + "name": "six_weeks", + "in": "query", + "required": false, + "description": "Specific six weeks period to retrieve assignments for", + "schema": { + "type": "string" + }, + "example": "1" + }, + "NoCache": { + "name": "no_cache", + "in": "query", + "required": false, + "description": "Bypass cache and fetch fresh data", + "schema": { + "type": "boolean", + "default": false + }, + "example": true + } + }, + "schemas": { + "WelcomeMessage": { + "type": "object", + "properties": { + "title": { + "type": "string", + "example": "Welcome to the Home Access Center API!" + }, + "message": { + "type": "string", + "example": "Interactive API documentation available at /docs" + }, + "docs_url": { + "type": "string", + "example": "https://hac.packjack.dev/docs" + }, + "openapi_spec": { + "type": "object", + "properties": { + "yaml": { + "type": "string", + "example": "https://hac.packjack.dev/openapi.yaml" + }, + "json": { + "type": "string", + "example": "https://hac.packjack.dev/openapi.json" + } + } + }, + "routes": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "/api/name", + "/api/info", + "/api/classes" + ] + }, + "cache_param": { + "type": "string", + "example": "Add ?no_cache=true to any endpoint to bypass cache" + } + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "description": "Error message describing what went wrong" + } + } + } + }, + "responses": { + "Unauthorized": { + "description": "Invalid username or password", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Invalid username or password" + } + } + } + }, + "InternalServerError": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Failed to fetch data from Home Access Center" + } + } + } + } + } + } +} diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..5303009 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,660 @@ +openapi: 3.0.3 +info: + title: Home Access Center API + description: | + A REST API for accessing Home Access Center student data. This API provides programmatic access to student information including grades, assignments, classes, transcripts, and more. + + ## Authentication + + All endpoints (except the root endpoint) require authentication via query parameters: + - `user`: Your Home Access Center username + - `pass`: Your Home Access Center password + + ## Caching + + The API implements intelligent caching: + - Login sessions are cached for 30 minutes + - Page data is cached for 5 minutes + - Add `?no_cache=true` to any endpoint to bypass cache + + ## Base URL + + This API is hosted at: `https://hac.packjack.dev` + version: 0.1.0 + contact: + name: Home Access Center API + license: + name: MIT + +servers: + - url: https://hac.packjack.dev + description: Production server + +tags: + - name: Student Info + description: Basic student information + - name: Classes + description: Class-related information + - name: Assignments + description: Assignment and grade data + - name: Reports + description: Report cards and transcripts + +paths: + /: + get: + summary: API Information + description: Returns welcome message and available routes + tags: + - Student Info + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: "Welcome to the Home Access Center API!" + message: + type: string + example: "Interactive API documentation available at /docs" + docs_url: + type: string + example: "https://hac.packjack.dev/docs" + openapi_spec: + type: object + properties: + yaml: + type: string + example: "https://hac.packjack.dev/openapi.yaml" + json: + type: string + example: "https://hac.packjack.dev/openapi.json" + routes: + type: array + items: + type: string + example: ["/api/name", "/api/info", "/api/classes"] + cache_param: + type: string + example: "Add ?no_cache=true to any endpoint to bypass cache" + + /api/: + get: + summary: API Information (alternate route) + description: Returns welcome message and available routes + tags: + - Student Info + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/WelcomeMessage' + + /docs: + get: + summary: Interactive API Documentation + description: Serves an interactive Swagger UI interface for exploring and testing the API + tags: + - Student Info + responses: + '200': + description: HTML page with Swagger UI + content: + text/html: + schema: + type: string + example: "..." + + /openapi.yaml: + get: + summary: OpenAPI Specification (YAML) + description: Returns the OpenAPI 3.0 specification in YAML format + tags: + - Student Info + responses: + '200': + description: OpenAPI specification in YAML format + content: + application/yaml: + schema: + type: string + + /openapi.json: + get: + summary: OpenAPI Specification (JSON) + description: Returns the OpenAPI 3.0 specification in JSON format + tags: + - Student Info + responses: + '200': + description: OpenAPI specification in JSON format + content: + application/json: + schema: + type: object + + /api/name: + get: + summary: Get Student Name + description: Retrieves the student's name from Home Access Center + tags: + - Student Info + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/info: + get: + summary: Get Student Information + description: Retrieves detailed student profile information + tags: + - Student Info + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + cohort_year: + type: string + example: "2027" + counselor: + type: string + example: "Jones, Natalie" + dob: + type: string + example: "5/2/2009" + grade: + type: string + example: "11" + language: + type: string + example: "English" + name: + type: string + example: "MacGregor, Jackson North" + school: + type: string + example: "Seven Lakes HS" + example: + cohort_year: "2027" + counselor: "Jones, Natalie" + dob: "5/2/2009" + grade: "11" + language: "English" + name: "MacGregor, Jackson North" + school: "Seven Lakes HS" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/classes: + get: + summary: Get Class List + description: Retrieves a list of the student's classes + tags: + - Classes + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/Short' + - $ref: '#/components/parameters/SixWeeks' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + type: string + example: ["AP Calculus BC", "English III Honors", "AP Chemistry"] + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/averages: + get: + summary: Get Class Averages + description: Retrieves current averages for all classes + tags: + - Classes + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/Short' + - $ref: '#/components/parameters/SixWeeks' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + additionalProperties: + type: string + example: + "AP Calculus BC": "95" + "English III Honors": "92" + "AP Chemistry": "88" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/assignments: + get: + summary: Get Assignments + description: Retrieves detailed assignment information for all classes + tags: + - Assignments + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/Short' + - $ref: '#/components/parameters/SixWeeks' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: array + items: + type: string + example: + "AP Calculus BC": + - ["10/15/2024", "Quiz 3", "Major Grades", "95", "100"] + - ["10/10/2024", "Homework 5", "Daily Grades", "10", "10"] + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/gradebook: + get: + summary: Get Complete Gradebook + description: Retrieves assignments with grades and weightings for all classes + tags: + - Assignments + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/Short' + - $ref: '#/components/parameters/SixWeeks' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + assignments: + type: array + items: + type: array + items: + type: string + weightings: + type: array + items: + type: array + items: + type: string + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/weightings: + get: + summary: Get Grade Weightings + description: Retrieves grade category weightings for all classes + tags: + - Classes + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/Short' + - $ref: '#/components/parameters/SixWeeks' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: array + items: + type: string + description: "Array containing: [category, points_earned, total_points, percentage, weight, weighted_value]" + example: + "AP PHYSICS 1 GT": + - ["Major", "187.0000", "200.00", "93.500%", "70.00", "65.450000"] + - ["Minor", "658.0000", "800.00", "82.250%", "20.00", "16.450000"] + - ["Other", "197.0000", "400.00", "49.250%", "10.00", "4.925000"] + "AP CALCULUS BC": + - ["Major", "162.0000", "200.00", "81.000%", "70.00", "56.700000"] + - ["Minor", "348.0000", "400.00", "87.000%", "20.00", "17.400000"] + - ["Other", "1397.0000", "1400.00", "99.785%", "10.00", "9.978600"] + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/reportcard: + get: + summary: Get Report Card + description: Retrieves report card tables with grades per marking period + tags: + - Reports + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + type: array + items: + type: string + example: + - ["Course", "Teacher", "MP1", "MP2", "Sem1", "MP3", "MP4", "Sem2", "Final"] + - ["AP Calculus BC", "Smith, John", "95", "93", "94", "96", "94", "95", "95"] + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/ipr: + get: + summary: Get Interim Progress Report + description: Retrieves interim progress report data + tags: + - Reports + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: array + items: + type: array + items: + type: string + example: + - ["Course", "Teacher", "Grade", "Absences", "Tardies"] + - ["AP Calculus BC", "Smith, John", "95", "0", "0"] + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/transcript: + get: + summary: Get Full Transcript + description: Retrieves complete transcript with GPA and semester information + tags: + - Reports + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + rank: + type: string + example: "15 of 450" + quartile: + type: string + example: "Top 25%" + Weighted GPA: + type: string + example: "4.35" + Unweighted GPA: + type: string + example: "3.95" + additionalProperties: + type: object + properties: + year: + type: string + semester: + type: string + credits: + type: string + data: + type: array + items: + type: array + items: + type: string + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/rank: + get: + summary: Get GPA Rank + description: Retrieves GPA rank and quartile information + tags: + - Reports + parameters: + - $ref: '#/components/parameters/Username' + - $ref: '#/components/parameters/Password' + - $ref: '#/components/parameters/Link' + - $ref: '#/components/parameters/NoCache' + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + rank: + type: string + example: "15 of 450" + quartile: + type: string + example: "Top 25%" + Weighted GPA: + type: string + example: "4.35" + Unweighted GPA: + type: string + example: "3.95" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + parameters: + Username: + name: user + in: query + required: true + description: Home Access Center username + schema: + type: string + example: "student123" + + Password: + name: pass + in: query + required: true + description: Home Access Center password + schema: + type: string + format: password + example: "mypassword" + + Link: + name: link + in: query + required: false + description: Home Access Center base URL (defaults to https://homeaccess.katyisd.org) + schema: + type: string + format: uri + default: https://homeaccess.katyisd.org + example: "https://homeaccess.katyisd.org" + + Short: + name: short + in: query + required: false + description: Whether to return shortened class names + schema: + type: boolean + default: false + example: true + + SixWeeks: + name: six_weeks + in: query + required: false + description: Specific six weeks period to retrieve assignments for + schema: + type: string + example: "1" + + NoCache: + name: no_cache + in: query + required: false + description: Bypass cache and fetch fresh data + schema: + type: boolean + default: false + example: true + + schemas: + WelcomeMessage: + type: object + properties: + title: + type: string + example: "Welcome to the Home Access Center API!" + message: + type: string + example: "Interactive API documentation available at /docs" + docs_url: + type: string + example: "https://hac.packjack.dev/docs" + openapi_spec: + type: object + properties: + yaml: + type: string + example: "https://hac.packjack.dev/openapi.yaml" + json: + type: string + example: "https://hac.packjack.dev/openapi.json" + routes: + type: array + items: + type: string + example: ["/api/name", "/api/info", "/api/classes"] + cache_param: + type: string + example: "Add ?no_cache=true to any endpoint to bypass cache" + + ErrorResponse: + type: object + properties: + error: + type: string + description: Error message describing what went wrong + + responses: + Unauthorized: + description: Invalid username or password + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "Invalid username or password" + + InternalServerError: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: "Failed to fetch data from Home Access Center" diff --git a/src/handlers.rs b/src/handlers.rs index 57e5f2c..1e426fc 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -3,6 +3,7 @@ use axum::{ http::StatusCode, response::Json, response::IntoResponse, + http::header, }; use serde_json::json; use serde::Deserialize; @@ -186,7 +187,12 @@ macro_rules! endpoint { pub async fn root() -> impl IntoResponse { let message = json!({ "title": "Welcome to the Home Access Center API!", - "message": "Visit the docs at https://homeaccesscenterapi-docs.vercel.app/", + "message": "Interactive API documentation available at /docs", + "docs_url": "https://hac.packjack.dev/docs", + "openapi_spec": { + "yaml": "https://hac.packjack.dev/openapi.yaml", + "json": "https://hac.packjack.dev/openapi.json" + }, "routes": [ "/api/name", "/api/assignments", "/api/info", "/api/averages", "/api/weightings", "/api/classes", "/api/reportcard", "/api/ipr", "/api/transcript", "/api/rank" ], @@ -195,6 +201,71 @@ pub async fn root() -> impl IntoResponse { Json(message) } +pub async fn serve_openapi_yaml() -> impl IntoResponse { + let openapi_content = include_str!("../openapi.yaml"); + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/yaml")], + openapi_content + ) +} + +pub async fn serve_openapi_json() -> impl IntoResponse { + let openapi_content = include_str!("../openapi.json"); + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "application/json")], + openapi_content + ) +} + +pub async fn serve_docs() -> impl IntoResponse { + let html = r#" + + + + + + Home Access Center API Documentation + + + + +
+ + + + + +"#; + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "text/html; charset=utf-8")], + html + ) +} + endpoint!( get_classes, assignments_page_scraper: extract_classes diff --git a/src/routes.rs b/src/routes.rs index edc6d43..8a09f7a 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,11 +1,14 @@ use axum::{routing::get, Router}; use crate::cache::Cache; -use crate::handlers::{root, get_averages, get_classes, get_info, get_name, get_assignments, get_gradebook, get_weightings, get_report_card, get_progress_report, get_transcript, get_rank}; +use crate::handlers::{root, get_averages, get_classes, get_info, get_name, get_assignments, get_gradebook, get_weightings, get_report_card, get_progress_report, get_transcript, get_rank, serve_openapi_yaml, serve_openapi_json, serve_docs}; pub fn create_router(cache: Cache) -> Router { Router::new() .route("/", get(root)) .route("/api/", get(root)) + .route("/docs", get(serve_docs)) + .route("/openapi.yaml", get(serve_openapi_yaml)) + .route("/openapi.json", get(serve_openapi_json)) .route("/api/name", get(get_name)) .route("/api/info", get(get_info)) .route("/api/classes", get(get_classes)) diff --git a/test_endpoints.sh b/test_endpoints.sh new file mode 100755 index 0000000..7116d65 --- /dev/null +++ b/test_endpoints.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# Test script to call all Home Access Center API endpoints +# Usage: ./test_endpoints.sh [base_url] +# +# Examples: +# ./test_endpoints.sh myuser mypass +# ./test_endpoints.sh myuser mypass https://homeaccess.katyisd.org + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if required arguments are provided +if [ $# -lt 2 ]; then + echo -e "${RED}Error: Missing required arguments${NC}" + echo "Usage: $0 [base_url] [api_url]" + echo "" + echo "Arguments:" + echo " username - Home Access Center username" + echo " password - Home Access Center password" + echo " base_url - (Optional) Home Access Center base URL (default: https://homeaccess.katyisd.org)" + echo " api_url - (Optional) API server URL (default: http://localhost:3000)" + echo "" + echo "Example:" + echo " $0 student123 mypassword" + echo " $0 student123 mypassword https://homeaccess.katyisd.org http://localhost:3000" + exit 1 +fi + +# Parse arguments +USERNAME="$1" +PASSWORD="$2" +HAC_BASE_URL="${3:-https://homeaccess.katyisd.org}" +API_URL="${4:-http://localhost:3000}" + +# Output file +OUTPUT_FILE="api_responses_$(date +%Y%m%d_%H%M%S).txt" + +# Create separator function +print_separator() { + echo "================================================================================" +} + +# Function to make API call and save response +call_endpoint() { + local endpoint="$1" + local description="$2" + local extra_params="$3" + + echo -e "${YELLOW}Testing: $endpoint - $description${NC}" + + # Build URL with parameters + local url="${API_URL}${endpoint}?user=${USERNAME}&pass=${PASSWORD}&link=${HAC_BASE_URL}" + + # Add extra parameters if provided + if [ -n "$extra_params" ]; then + url="${url}&${extra_params}" + fi + + # Make request and capture response with timeout + local http_code + local response + + response=$(curl -s -m 30 -w "\nHTTP_STATUS:%{http_code}" "$url" 2>&1) + http_code=$(echo "$response" | grep "HTTP_STATUS:" | cut -d: -f2) + response=$(echo "$response" | sed '/HTTP_STATUS:/d') + + # Write to output file + { + print_separator + echo "ENDPOINT: $endpoint" + echo "DESCRIPTION: $description" + echo "URL: $url" + echo "TIMESTAMP: $(date '+%Y-%m-%d %H:%M:%S')" + echo "HTTP STATUS: $http_code" + print_separator + echo "" + echo "$response" + echo "" + echo "" + } >> "$OUTPUT_FILE" + + # Print status + if [ "$http_code" -eq 200 ]; then + echo -e "${GREEN}✓ Success (HTTP $http_code)${NC}" + else + echo -e "${RED}✗ Failed (HTTP $http_code)${NC}" + fi + echo "" +} + +# Start test +echo "========================================================" +echo " Home Access Center API Endpoint Test" +echo "========================================================" +echo "" +echo "API URL: $API_URL" +echo "HAC URL: $HAC_BASE_URL" +echo "Username: $USERNAME" +echo "Output file: $OUTPUT_FILE" +echo "" +echo "Starting tests..." +echo "" + +# Initialize output file +{ + echo "========================================================================" + echo " HOME ACCESS CENTER API - ENDPOINT TEST RESULTS" + echo "========================================================================" + echo "" + echo "Test Date: $(date '+%Y-%m-%d %H:%M:%S')" + echo "API URL: $API_URL" + echo "HAC Base URL: $HAC_BASE_URL" + echo "Username: $USERNAME" + echo "" + echo "This file contains responses from all API endpoints." + echo "" +} > "$OUTPUT_FILE" + +# Test root endpoint (no auth required) +echo -e "${YELLOW}Testing: / - Root endpoint${NC}" +response=$(curl -s -m 10 "${API_URL}/" 2>&1) +{ + print_separator + echo "ENDPOINT: /" + echo "DESCRIPTION: Root endpoint (no authentication)" + echo "URL: ${API_URL}/" + echo "TIMESTAMP: $(date '+%Y-%m-%d %H:%M:%S')" + print_separator + echo "" + echo "$response" + echo "" + echo "" +} >> "$OUTPUT_FILE" +echo -e "${GREEN}✓ Success${NC}" +echo "" + +# Test all authenticated endpoints +call_endpoint "/api/name" "Get student name" +call_endpoint "/api/info" "Get student information" +call_endpoint "/api/classes" "Get list of classes" +call_endpoint "/api/classes" "Get list of classes (short names)" "short=true" +call_endpoint "/api/averages" "Get class averages" +call_endpoint "/api/averages" "Get class averages (short names)" "short=true" +call_endpoint "/api/assignments" "Get detailed assignments" +call_endpoint "/api/assignments" "Get detailed assignments (short names)" "short=true" +call_endpoint "/api/gradebook" "Get complete gradebook" +call_endpoint "/api/gradebook" "Get complete gradebook (short names)" "short=true" +call_endpoint "/api/weightings" "Get grade weightings" +call_endpoint "/api/weightings" "Get grade weightings (short names)" "short=true" +call_endpoint "/api/reportcard" "Get report card" +call_endpoint "/api/ipr" "Get interim progress report" +call_endpoint "/api/transcript" "Get full transcript" +call_endpoint "/api/rank" "Get GPA rank and quartile" + +# Summary +echo "========================================================" +echo " Test Complete!" +echo "========================================================" +echo "" +echo "All responses have been saved to: $OUTPUT_FILE" +echo "" +echo "You can view the results with:" +echo " cat $OUTPUT_FILE" +echo " less $OUTPUT_FILE" +echo ""