Skip to content
Merged

Dev #110

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/accessibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Accessibility Tests

on:
pull_request:
push:
# branches: [main, dev]
workflow_dispatch:

jobs:
a11y:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install backend deps
# Install python dependencies from project.toml
run: cd server && pip install --no-cache-dir ./

- name: Install frontend deps
run: npm ci

- name: Build frontend
run: npm run build

- name: Start Flask backend
env:
# Set testing environment variables
CI: 'true'
FLASK_ENV: testing
run: |
python -m server.run &
sleep 5

- name: Start frontend
run: |
npm run preview &
sleep 5
env:
FRONTEND_PORT: 4173

- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Run Playwright accessibility tests
run: npx playwright test
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ db-changes


public/config.json
auth.json
# Playwright output
test-results/
playwright-report/
88 changes: 88 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
"vue-tsc": "^2.2.10"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.0",
"@eslint/js": "^9.14.0",
"@playwright/test": "^1.57.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"eslint": "^9.14.0",
Expand Down
19 changes: 19 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './tests',

projects: [
{
name: 'setup',
testMatch: /auth\.setup\.js/,
},
{
name: 'chromium',
use: {
storageState: 'auth.json',
},
dependencies: ['setup'],
},
],
});
12 changes: 8 additions & 4 deletions server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
import os
import sys

from flask import Flask
from flask_jwt_extended import JWTManager

from server.config import Config
from server.config import get_config
from server.extensions import cors
from server.register_routes import register_routes
from server.routes.auth import auth_bp


def create_app():
# Allow insecure transport for testing purposes (for SSO) - DO NOT USE IN PRODUCTION!!
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

app = Flask(__name__)

config_class = get_config()
app.config.from_object(get_config())

# Allows cross-origin requests from your Vue frontend
cors.init_app(
app,
supports_credentials=True,
expose_headers=['X-CSRF-TOKEN'],
allow_headers=['Content-Type', 'X-CSRF-TOKEN'],
)
app.config.from_object(Config)

# Register blueprints (modular routes)
from server.register_routes import register_routes

register_routes(app)

jwt = JWTManager(app)
Expand Down
16 changes: 16 additions & 0 deletions server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ class Config:

# Power BI Expected API Key
EXPECTED_API_KEY = os.environ.get('EXPECTED_API_KEY')

# Testing Authentication Mode
TEST_AUTH_ENABLED = False


class TestingConfig(Config):
TEST_AUTH_ENABLED = True


def get_config():
"""
Decide config based on environment.
"""
if os.getenv('CI') == 'true':
return TestingConfig
return Config
13 changes: 11 additions & 2 deletions server/register_routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Flask

from server.routes.auth import auth_bp
from server.config import get_config
from server.routes.data import data_bp
from server.routes.powerbi import powerbi_bp
from server.routes.report import report_bp
Expand All @@ -14,9 +14,18 @@ def register_routes(app: Flask):

This helps in keeping the main app configuration clean and modular.
"""
app.register_blueprint(auth_bp, url_prefix='/api/auth')

app.register_blueprint(data_bp, url_prefix='/api/data')
app.register_blueprint(user_bp, url_prefix='/api/user')
app.register_blueprint(report_bp, url_prefix='/api/report')
app.register_blueprint(stats_bp, url_prefix='/api/stats')
app.register_blueprint(powerbi_bp, url_prefix='/api/powerbi')
# Register test auth routes only if testing mode is enabled
if get_config().TEST_AUTH_ENABLED:
from server.routes.auth_test import auth_test_bp

app.register_blueprint(auth_test_bp, url_prefix='/api/auth_test')
else:
from server.routes.auth import auth_bp

app.register_blueprint(auth_bp, url_prefix='/api/auth')
28 changes: 19 additions & 9 deletions server/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,32 @@

auth_bp = Blueprint('auth', __name__)

AUTHORITY = f'https://login.microsoftonline.com/{Config.TENANT_ID}'

SCOPES = []

# Initialize MSAL
msal_app = msal.ConfidentialClientApplication(
Config.CLIENT_ID, authority=AUTHORITY, client_credential={Config.CLIENT_SECRET}
)

def get_msal_app():
"""
Lazily create the MSAL app only when Azure auth is actually used.
"""
if app.config.get('TEST_AUTH_ENABLED'):
# Don't use Azure in CI mode
raise RuntimeError('MSAL should not be used in CI mode')

authority = f'https://login.microsoftonline.com/{Config.TENANT_ID}'

return msal.ConfidentialClientApplication(
Config.CLIENT_ID,
authority=authority,
client_credential=Config.CLIENT_SECRET,
)


@auth_bp.route('/login')
def login():
"""
Redirects user to Microsoft Login page.
"""
msal_app = get_msal_app()
auth_url = msal_app.get_authorization_request_url(
SCOPES, redirect_uri=app.config.get('REDIRECT_URI')
)
Expand All @@ -45,6 +56,7 @@ def auth_redirect():
"""
Handles Azure AD login redirect.
"""
msal_app = get_msal_app()
code = request.args.get('code')
if not code:
return jsonify({'error': 'No auth code provided'}), 400
Expand Down Expand Up @@ -261,9 +273,7 @@ def get_user_by_email():
"""
Look up a user in Azure AD by email address.
"""
# email = request.args.get('email')
# if not email:
# return jsonify({'error': 'Email parameter is required'}), 400
msal_app = get_msal_app()
data = request.get_json()
email = data.get('email')
if not email:
Expand Down
Loading
Loading