forked from colin/resume
Add download as PDF button and update test configurations
This commit is contained in:
parent
4f9596bbee
commit
04e5a9fa34
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# Deployment Workflow
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
- All changes must be tested locally before deployment
|
||||||
|
- Tests must pass for both mobile and desktop viewports
|
||||||
|
- Lighthouse tests must maintain perfect scores (100/100) for accessibility and SEO
|
||||||
|
- Playwright tests must pass for all viewport sizes
|
||||||
|
|
||||||
|
## Git Operations
|
||||||
|
- Avoid use direct git commands (`git add`, `git commit`, `git push`) if we do git flows do then as oneliners.
|
||||||
|
- All git operations must be performed through `./build-test-deploy.sh`
|
||||||
|
- The script handles:
|
||||||
|
- Building the Docker container
|
||||||
|
- Running all tests
|
||||||
|
- Committing changes
|
||||||
|
- Pushing to repository
|
||||||
|
|
||||||
|
## Build-Test-Deploy Script
|
||||||
|
The `./build-test-deploy.sh` script is the single source of truth for deployment:
|
||||||
|
1. Builds the Docker container
|
||||||
|
2. Runs the test suite
|
||||||
|
3. Only proceeds with git operations if tests pass
|
||||||
|
4. Handles all git operations in the correct order
|
||||||
|
|
||||||
|
## Testing Process
|
||||||
|
1. Make changes to the codebase
|
||||||
|
2. Run `./build-test-deploy.sh` to:
|
||||||
|
- Build the container
|
||||||
|
- Run tests
|
||||||
|
- Deploy if tests pass
|
||||||
|
3. If tests fail:
|
||||||
|
- Fix the issues
|
||||||
|
- Run the script again
|
||||||
|
- Do not proceed with deployment until all tests pass
|
||||||
|
|
||||||
|
## Viewport Testing
|
||||||
|
The test suite runs against multiple viewport sizes:
|
||||||
|
- Mobile: 375x667
|
||||||
|
- Desktop: 1024x768
|
||||||
|
|
||||||
|
All tests must pass for all viewport sizes before deployment is allowed.
|
||||||
|
|
||||||
|
## Lighthouse Requirements
|
||||||
|
Maintain perfect scores:
|
||||||
|
- Accessibility: 100/100
|
||||||
|
- SEO: 100/100
|
||||||
|
- Performance: 97/100 minimum
|
||||||
|
- Best Practices: 93/100 minimum
|
||||||
|
|
||||||
|
## Playwright Tests
|
||||||
|
Must pass all accessibility tests:
|
||||||
|
- WCAG 2.1 Level AAA compliance
|
||||||
|
- ARIA attributes
|
||||||
|
- Heading structure
|
||||||
|
- External link validation
|
||||||
|
- Color contrast
|
||||||
|
- Image alt text
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
IMAGE_NAME="resume:latest"
|
||||||
|
CONTAINER_NAME="resume_test_container"
|
||||||
|
DOCKER_DIR="docker"
|
||||||
|
RESUME_DIR="$DOCKER_DIR/resume"
|
||||||
|
|
||||||
|
# Recalculate SHA256 hash for styles.css and update integrity in index.html
|
||||||
|
STYLES_HASH=$(shasum -a 256 $RESUME_DIR/styles.css | awk '{print $1}' | xxd -r -p | base64)
|
||||||
|
sed -i -E "s|href=\"styles.css\" integrity=\"sha256-[^\"]*\"|href=\"styles.css\" integrity=\"sha256-$STYLES_HASH\"|" $RESUME_DIR/index.html
|
||||||
|
|
||||||
|
# Verify the hash update
|
||||||
|
if ! grep -q "integrity=\"sha256-$STYLES_HASH\"" $RESUME_DIR/index.html; then
|
||||||
|
echo "Error: Integrity hash update failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test the CSS loading
|
||||||
|
if ! curl -s http://localhost:8080/styles.css > /dev/null; then
|
||||||
|
echo "Error: CSS file not accessible."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the hash in the container
|
||||||
|
CONTAINER_HASH=$(docker exec $CONTAINER_NAME cat /srv/styles.css | shasum -a 256 | awk '{print $1}' | xxd -r -p | base64)
|
||||||
|
if [ "$CONTAINER_HASH" != "$STYLES_HASH" ]; then
|
||||||
|
echo "Error: Integrity hash mismatch in container."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
cd "$DOCKER_DIR"
|
||||||
|
echo "Building Docker image..."
|
||||||
|
docker build -t $IMAGE_NAME ./resume/
|
||||||
|
|
||||||
|
# Stop and remove any previous container
|
||||||
|
if [ $(docker ps -aq -f name=$CONTAINER_NAME) ]; then
|
||||||
|
echo "Removing previous test container..."
|
||||||
|
docker rm -f $CONTAINER_NAME || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure port 8080 is free
|
||||||
|
echo "Ensuring port 8080 is free..."
|
||||||
|
lsof -i :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9 || true
|
||||||
|
|
||||||
|
# Run Docker container in the background
|
||||||
|
echo "Starting Docker container..."
|
||||||
|
docker run -d --name $CONTAINER_NAME -p 8080:8080 $IMAGE_NAME
|
||||||
|
|
||||||
|
# Wait for the server to be ready
|
||||||
|
MAX_TRIES=20
|
||||||
|
TRIES=0
|
||||||
|
until curl -s http://localhost:8080/ > /dev/null; do
|
||||||
|
TRIES=$((TRIES+1))
|
||||||
|
if [ $TRIES -ge $MAX_TRIES ]; then
|
||||||
|
echo "Server did not start in time."
|
||||||
|
docker rm -f $CONTAINER_NAME
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Waiting for server... ($TRIES/$MAX_TRIES)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Server is up. Running tests..."
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm run setup
|
||||||
|
if npm test; then
|
||||||
|
echo "Tests passed. Committing and pushing changes."
|
||||||
|
git add .
|
||||||
|
git commit -m "Automated build, test, and deploy"
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "Tests failed. Not deploying."
|
||||||
|
docker rm -f $CONTAINER_NAME
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cleaning up Docker container..."
|
||||||
|
docker rm -f $CONTAINER_NAME
|
||||||
|
|
||||||
|
echo "Done."
|
|
@ -1,4 +1,4 @@
|
||||||
:8080 {
|
colinknapp.com {
|
||||||
root * .
|
root * .
|
||||||
file_server
|
file_server
|
||||||
encode gzip
|
encode gzip
|
||||||
|
@ -10,7 +10,57 @@
|
||||||
-X-Powered-By
|
-X-Powered-By
|
||||||
|
|
||||||
# HSTS
|
# HSTS
|
||||||
# Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
|
||||||
|
# Basic security headers
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Permissions policy
|
||||||
|
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
|
||||||
|
|
||||||
|
# Cross-origin isolation headers
|
||||||
|
Cross-Origin-Embedder-Policy "require-corp"
|
||||||
|
Cross-Origin-Resource-Policy "same-origin"
|
||||||
|
Cross-Origin-Opener-Policy "same-origin"
|
||||||
|
|
||||||
|
# Cache control for static assets
|
||||||
|
Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# CSP with hashes for scripts and styles
|
||||||
|
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8='; style-src 'self' 'sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle 404s
|
||||||
|
handle_errors {
|
||||||
|
respond "{err.status_code} {err.status_text}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable static file serving with caching
|
||||||
|
file_server {
|
||||||
|
precompressed
|
||||||
|
browse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Local development server
|
||||||
|
:8080 {
|
||||||
|
root * .
|
||||||
|
file_server
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
# Performance optimizations
|
||||||
|
header {
|
||||||
|
# Remove default Caddy headers
|
||||||
|
-Server
|
||||||
|
-X-Powered-By
|
||||||
|
|
||||||
# Basic security headers
|
# Basic security headers
|
||||||
X-Frame-Options "DENY"
|
X-Frame-Options "DENY"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Colin Knapp - Cybersecurity Expert and Software Developer Portfolio">
|
<meta name="description" content="Colin Knapp - Cybersecurity Expert and Software Developer Portfolio">
|
||||||
<title>Colin Knapp Portfolio</title>
|
<title>Colin Knapp Portfolio</title>
|
||||||
<link rel="stylesheet" href="styles.css" integrity="sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI=" crossorigin="anonymous">
|
<link rel="stylesheet" href="styles.css" integrity="sha256-Ps1dklCHzk1leTAfqkeA64YDuDJxx5QZBjC2UQhSdz0=" crossorigin="anonymous">
|
||||||
<script src="theme.js" integrity="sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=" crossorigin="anonymous"></script>
|
<script src="theme.js" integrity="sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=" crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -18,6 +18,13 @@
|
||||||
title="Toggle between light, dark, and auto theme modes"
|
title="Toggle between light, dark, and auto theme modes"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>🌓</button>
|
>🌓</button>
|
||||||
|
<button
|
||||||
|
id="downloadPDF"
|
||||||
|
aria-label="Download as PDF"
|
||||||
|
title="Download resume as PDF"
|
||||||
|
tabindex="0"
|
||||||
|
onclick="downloadAsPDF()"
|
||||||
|
>📄</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid" role="main">
|
<div class="container-fluid" role="main">
|
||||||
|
@ -195,5 +202,24 @@
|
||||||
|
|
||||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
function downloadAsPDF() {
|
||||||
|
// Store current theme
|
||||||
|
const currentTheme = document.body.getAttribute('data-theme');
|
||||||
|
|
||||||
|
// Force light theme for PDF
|
||||||
|
document.body.setAttribute('data-theme', 'light');
|
||||||
|
|
||||||
|
// Wait for theme change to apply
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
|
||||||
|
// Restore original theme
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.setAttribute('data-theme', currentTheme);
|
||||||
|
}, 100);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -0,0 +1,225 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Colin Knapp - Cybersecurity Expert and Software Developer Portfolio">
|
||||||
|
<title>Colin Knapp Portfolio</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" integrity="sha256-Ps1dklCHzk1leTAfqkeA64YDuDJxx5QZBjC2UQhSdz0=" crossorigin="anonymous">
|
||||||
|
<script src="theme.js" integrity="sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=" crossorigin="anonymous"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="theme-switch">
|
||||||
|
<button
|
||||||
|
id="themeToggle"
|
||||||
|
aria-label="Theme mode: Auto"
|
||||||
|
role="switch"
|
||||||
|
aria-checked="false"
|
||||||
|
title="Toggle between light, dark, and auto theme modes"
|
||||||
|
tabindex="0"
|
||||||
|
>🌓</button>
|
||||||
|
<button
|
||||||
|
id="downloadPDF"
|
||||||
|
aria-label="Download as PDF"
|
||||||
|
title="Download resume as PDF"
|
||||||
|
tabindex="0"
|
||||||
|
onclick="downloadAsPDF()"
|
||||||
|
>📄</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p><strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong> <a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a><br>
|
||||||
|
<strong>Schedule a Meeting:</strong> <a href="https://cal.com/colink/30min" target="_blank">30 Minute Meeting</a></p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Highlights & Measurables</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Cybersecurity Leadership:</strong> Currently spearheading <em><a href="http://ViperWire.ca">ViperWire.ca</a></em>, the public-facing arm of my AI-powered cybersecurity and development consultancy, delivering cutting-edge protection for digital assets (2023-Present).</li>
|
||||||
|
<li><strong>Open-Source Impact:</strong> Co-created <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, revolutionizing Minecraft development by enabling massive transformative edits—scaling from 50,000 server-crashing edits to billions without interruption—powering a $2 billion game brand with global contributor support (2014-Present).</li>
|
||||||
|
<li><strong>Team Leadership:</strong> Managed a distributed team of 45 contractors at NitricConcepts, fostering collaboration and deploying advanced DevSecOps practices (2018-2021).</li>
|
||||||
|
<li><strong>On-Premises Innovation:</strong> Architected self-managed, bare-metal infrastructure with orchestration for on-premises deployments, delivering performant, scalable systems compliant with WCAG 2.0 AA for clients like <a href="https://showerloop.cc">ShowerLoop</a>, meeting stringent government accessibility and compliance goals (2020-Present).</li>
|
||||||
|
<li><strong>Government Projects:</strong> Delivered scalable, secure learning management systems for the US government and consulted on <a href="https://bishopairport.org">Flint Bishop International Airport</a>'s website and domain infrastructure via Addis Enterprises, building a geographically redundant DNS cluster with an A+ standard resilient to extreme scenarios (2019-Present).</li>
|
||||||
|
<li><strong>Healthcare Infrastructure:</strong> Developed and deployed infrastructure for <a href="https://improvingmipractices.org">Improving MI Practices</a>, a critical healthcare education platform, ensuring high availability and security for sensitive medical training content (2023-Present).</li>
|
||||||
|
<li><strong>Security Automation:</strong> Created a Docker-based utility for automated WordPress malware removal and hardening, successfully deployed to protect <a href="https://mlpp.org">MLPP</a> from persistent cyber attacks, reducing infection frequency from daily to zero (2023).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Project Experience</h2>
|
||||||
|
<h3>DevSecOps at Addis Enterprises</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||||
|
<strong>Overview:</strong> Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Partnered with senior professionals to deliver learning management systems meeting WCAG 2.0 AA compliance for government clients.</li>
|
||||||
|
<li>Consulted for Flint Bishop International Airport, architecting a geographically redundant DNS cluster achieving an A+ standard, capable of withstanding extreme disruptions.</li>
|
||||||
|
<li>Provided exceptional client service through effective communication and tailored solutions.<br>
|
||||||
|
<strong>Impact:</strong> Strengthened government digital infrastructure and ensured robust, resilient airport domain systems.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Healthcare Platform Infrastructure</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||||
|
<strong>Overview:</strong> Led infrastructure design and operations for <a href="https://improvingmipractices.org">Improving MI Practices</a> (<a href="https://archive.is/D5HIb">archive</a>) through Addis Enterprises, a critical healthcare education platform.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Designed and implemented secure, scalable infrastructure for healthcare education content delivery.</li>
|
||||||
|
<li>Administered CIS Level 1 and 2 security standards implementation for enhanced system hardening and security controls.</li>
|
||||||
|
<li>Implemented automated deployment pipelines and monitoring systems for high availability.<br>
|
||||||
|
<strong>Impact:</strong> Enabled reliable delivery of critical healthcare training content to medical professionals while maintaining robust security standards.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>WordPress Security Automation</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2023<br>
|
||||||
|
<strong>Overview:</strong> Developed an automated solution for WordPress malware removal and hardening.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Created a Docker-based utility for automated malware detection and removal.</li>
|
||||||
|
<li>Implemented hardening measures to prevent reinfection.</li>
|
||||||
|
<li>Successfully deployed to protect MLPP from persistent cyber attacks.<br>
|
||||||
|
<strong>Impact:</strong> Reduced infection frequency from daily/weekly to zero, significantly improving site security and reliability.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>YouTube Game Development & Cybersecurity</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2009-2022<br>
|
||||||
|
<strong>Overview:</strong> Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Built immersive gaming experiences for large audiences.</li>
|
||||||
|
<li>Implemented DDoS defense, anti-phishing protocols, and data privacy measures.</li>
|
||||||
|
<li>Managed hardware/software lifecycles and created comprehensive documentation.<br>
|
||||||
|
<strong>Impact:</strong> Delivered secure, seamless gaming experiences to millions of users.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Web Design & Java Plugin Development</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2009-2023<br>
|
||||||
|
<strong>Overview:</strong> Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Utilized Jenkins and GitLab CI/CD for streamlined workflows, leveraging a robust toolchain for rapid development.</li>
|
||||||
|
<li>Managed complex systems and ensured WCAG 2.0 AA accessibility standards.</li>
|
||||||
|
<li>Provided technical guidance and detailed client documentation, drawing on broad experience to resolve diverse issues.<br>
|
||||||
|
<strong>Impact:</strong> Enhanced project delivery speed and quality for diverse computing environments through prolific development practices.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>App Development for Influencers</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2013-2018<br>
|
||||||
|
<strong>Overview:</strong> Created an ad revenue tracking app to optimize earnings and strategies for content creators.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Designed a user-friendly tool for real-time revenue monitoring using an optimized toolchain for efficiency.</li>
|
||||||
|
<li>Ensured secure data handling and system performance with extensive problem-solving expertise.<br>
|
||||||
|
<strong>Impact:</strong> Empowered creators to maximize earnings and refine content strategies.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>DevOps & Co-Founder at NitricConcepts</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2018-2021<br>
|
||||||
|
<strong>Overview:</strong> Led a global team in building secure, scalable gaming solutions.<br>
|
||||||
|
<strong>Key Contributions:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Managed 45 contractors worldwide, implementing Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain.</li>
|
||||||
|
<li>Co-developed <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, enabling billions of seamless edits for Minecraft creators.</li>
|
||||||
|
<li>Fostered a collaborative, innovative team culture.<br>
|
||||||
|
<strong>Impact:</strong> Transformed NitricConcepts into a thriving multinational entity through prolific and efficient development.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Entrepreneurial Ventures</h3>
|
||||||
|
<h4><a href="http://Athion.net">Athion.net</a> Turnaround</h4>
|
||||||
|
<p><strong>Timeframe:</strong> 2013-2017<br>
|
||||||
|
<strong>Overview:</strong> Revitalized a struggling business into a self-sustaining operation in two weeks.<br>
|
||||||
|
<strong>Key Contributions:</strong> Optimized systems and streamlined operations with rapid, effective solutions.<br>
|
||||||
|
<strong>Impact:</strong> Created a profitable, independent venture.</p>
|
||||||
|
|
||||||
|
<h4><a href="http://MotherboardRepair.ca">MotherboardRepair.ca</a></h4>
|
||||||
|
<p><strong>Timeframe:</strong> 2019-Present<br>
|
||||||
|
<strong>Overview:</strong> Co-founded a company reducing e-waste through circuit board repairs.<br>
|
||||||
|
<strong>Key Contributions:</strong> Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.<br>
|
||||||
|
<strong>Impact:</strong> Promoted environmental responsibility in electronics.</p>
|
||||||
|
|
||||||
|
<h4><a href="https://showerloop.cc">ShowerLoop Project</a></h4>
|
||||||
|
<p><strong>Timeframe:</strong> 2016<br>
|
||||||
|
<strong>Overview:</strong> Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.<br>
|
||||||
|
<strong>Key Contributions:</strong> Designed and implemented a responsive, accessible website with improved user experience and technical documentation.<br>
|
||||||
|
<strong>Impact:</strong> Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Additional Information</h2>
|
||||||
|
<h3>Personal Development</h3>
|
||||||
|
<p><strong>Timeframe:</strong> 2009-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Self-Taught Mastery:</strong> Continuously honed cybersecurity and systems management skills, building a broad knowledge base to tackle unique challenges with a passion for innovation and problem-solving.</li>
|
||||||
|
<li><strong>Open-Source Contributions:</strong> Actively maintain smaller self-run open-source projects; previously led <em>OhMyForm</em> (retired in favor of FormBricks) and contributed to <em>PlotSquared</em>, <em>FastAsyncWorldEdit</em>, and <em>PlotHider</em>, reflecting a prolific commitment to advancing technology.</li>
|
||||||
|
<li><strong>Skill Maintenance:</strong> Regularly run Woodpecker CI and Gitea for on-premise source management, testing, and deployment, employing security scanning and unit testing to ensure core functionality and security baselines, alongside self-hosting exercises to sustain rapid, high-volume development capabilities across a vast array of innovative projects.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Relevant Links & Web Impact</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Repositories:</strong> <a href="https://github.com/IntellectualSites/PlotSquared">PlotSquared</a>, <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit">FastAsyncWorldEdit</a>, <a href="https://github.com/OhMyForm/OhMyForm">OhMyForm</a>, <a href="https://github.com/IntellectualSites/plothider">PlotHider</a></li>
|
||||||
|
<li><strong>Projects:</strong> <a href="https://viperwire.ca">ViperWire.ca</a>, <a href="https://nitricconcepts.com">NitricConcepts</a>, <a href="https://showerloop.cc">ShowerLoop</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="section" role="region" aria-labelledby="open-source-heading">
|
||||||
|
<h2 id="open-source-heading">Open Source & Infrastructure</h2>
|
||||||
|
<div class="entry">
|
||||||
|
<h3>PlotSquared & FastAsyncWorldEdit</h3>
|
||||||
|
<p class="date">2013-Present</p>
|
||||||
|
<p class="overview">Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Contributed to <a href="https://github.com/IntellectualSites/PlotSquared" target="_blank">PlotSquared</a>, a land management plugin with 572+ stars and 809+ forks</li>
|
||||||
|
<li>Enhanced <a href="https://github.com/IntellectualSites/FastAsyncWorldEdit" target="_blank">FastAsyncWorldEdit</a>, improving world manipulation performance with 664+ stars</li>
|
||||||
|
<li>Implemented security improvements and performance optimizations for large-scale server operations</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="entry">
|
||||||
|
<h3>Athion.net Infrastructure</h3>
|
||||||
|
<p class="date">2013-Present</p>
|
||||||
|
<p class="overview">Established and maintained critical infrastructure for Minecraft development community.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Set up and maintained <a href="https://ci.athion.net/" target="_blank">Jenkins CI/CD pipeline</a> since 2013, supporting continuous integration for game content development</li>
|
||||||
|
<li>Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation</li>
|
||||||
|
<li>Implemented robust security measures and performance optimizations for high-traffic development environments</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="experience-item">
|
||||||
|
<h3>Software Engineer</h3>
|
||||||
|
<p class="company">Oh My Form</p>
|
||||||
|
<p class="date">2020 - Present</p>
|
||||||
|
<p class="achievement">Led development of Oh My Form, achieving over 1.5 million Docker pulls as verified by <a href="https://hub.docker.com/u/ohmyform" target="_blank" rel="noopener noreferrer">Docker Hub</a> and <a href="https://archive.is/lZHAT" target="_blank" rel="noopener noreferrer">archived</a>.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Developed and maintained a secure, high-performance form builder application</li>
|
||||||
|
<li>Implemented robust security measures and best practices</li>
|
||||||
|
<li>Optimized application performance and user experience</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function downloadAsPDF() {
|
||||||
|
// Store current theme
|
||||||
|
const currentTheme = document.body.getAttribute('data-theme');
|
||||||
|
|
||||||
|
// Force light theme for PDF
|
||||||
|
document.body.setAttribute('data-theme', 'light');
|
||||||
|
|
||||||
|
// Wait for theme change to apply
|
||||||
|
setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
|
||||||
|
// Restore original theme
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.setAttribute('data-theme', currentTheme);
|
||||||
|
}, 100);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -144,27 +144,50 @@ hr {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
z-index: 100;
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#themeToggle {
|
.theme-switch button {
|
||||||
padding: 8px 16px;
|
background: none;
|
||||||
background-color: var(--theme-bg);
|
border: none;
|
||||||
border: 1px solid var(--theme-border);
|
font-size: 1.5em;
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
padding: 5px;
|
||||||
color: var(--text-color);
|
border-radius: 50%;
|
||||||
transition: all 0.3s ease;
|
transition: background-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#themeToggle:hover {
|
.theme-switch button:hover {
|
||||||
background-color: var(--theme-hover);
|
background-color: rgba(128, 128, 128, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#themeToggle:focus {
|
/* Print styles */
|
||||||
outline: 2px solid var(--accent-color);
|
@media print {
|
||||||
outline-offset: 2px;
|
.theme-switch {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: white !important;
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: black !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-fluid {
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -23,7 +23,8 @@
|
||||||
"author": "Colin Knapp",
|
"author": "Colin Knapp",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.42.1",
|
"@axe-core/playwright": "^4.10.1",
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
"chrome-launcher": "^1.1.2",
|
"chrome-launcher": "^1.1.2",
|
||||||
"lighthouse": "^11.4.0",
|
"lighthouse": "^11.4.0",
|
||||||
"puppeteer": "^22.4.1"
|
"puppeteer": "^22.4.1"
|
||||||
|
|
|
@ -4,6 +4,11 @@ const { AxeBuilder } = require('@axe-core/playwright');
|
||||||
const PRODUCTION_URL = 'https://colinknapp.com';
|
const PRODUCTION_URL = 'https://colinknapp.com';
|
||||||
const LOCAL_URL = 'http://localhost:8080';
|
const LOCAL_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
const viewports = [
|
||||||
|
{ width: 375, height: 667, name: 'mobile' },
|
||||||
|
{ width: 1024, height: 768, name: 'desktop' }
|
||||||
|
];
|
||||||
|
|
||||||
async function getPageUrl(page) {
|
async function getPageUrl(page) {
|
||||||
try {
|
try {
|
||||||
// Try production first
|
// Try production first
|
||||||
|
@ -16,10 +21,15 @@ async function getPageUrl(page) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Accessibility Tests', () => {
|
viewports.forEach(viewport => {
|
||||||
|
test.describe(`Accessibility Tests (${viewport.name})`, () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
});
|
||||||
|
|
||||||
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
|
test('should pass WCAG 2.1 Level AAA standards', async ({ page }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running accessibility tests against ${url}`);
|
console.log(`Running accessibility tests against ${url} on ${viewport.name}`);
|
||||||
|
|
||||||
// Run axe accessibility tests
|
// Run axe accessibility tests
|
||||||
const results = await new AxeBuilder({ page })
|
const results = await new AxeBuilder({ page })
|
||||||
|
@ -32,7 +42,7 @@ test.describe('Accessibility Tests', () => {
|
||||||
|
|
||||||
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
|
test('should have proper ARIA attributes for theme toggle', async ({ page }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running ARIA tests against ${url}`);
|
console.log(`Running ARIA tests against ${url} on ${viewport.name}`);
|
||||||
|
|
||||||
// Check theme toggle button
|
// Check theme toggle button
|
||||||
const themeToggle = await page.locator('#themeToggle');
|
const themeToggle = await page.locator('#themeToggle');
|
||||||
|
@ -45,7 +55,7 @@ test.describe('Accessibility Tests', () => {
|
||||||
|
|
||||||
test('should have proper heading structure', async ({ page }) => {
|
test('should have proper heading structure', async ({ page }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running heading structure tests against ${url}`);
|
console.log(`Running heading structure tests against ${url} on ${viewport.name}`);
|
||||||
|
|
||||||
// Check main content area
|
// Check main content area
|
||||||
const mainContent = await page.locator('.container-fluid');
|
const mainContent = await page.locator('.container-fluid');
|
||||||
|
@ -61,7 +71,7 @@ test.describe('Accessibility Tests', () => {
|
||||||
|
|
||||||
test('should have working external links', async ({ page, request }) => {
|
test('should have working external links', async ({ page, request }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running link validation tests against ${url}`);
|
console.log(`Running link validation tests against ${url} on ${viewport.name}`);
|
||||||
await page.goto(url, { timeout: 60000 });
|
await page.goto(url, { timeout: 60000 });
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
@ -74,34 +84,51 @@ test.describe('Accessibility Tests', () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set a longer timeout for external link checks
|
||||||
|
test.setTimeout(120000);
|
||||||
|
|
||||||
const brokenLinks = [];
|
const brokenLinks = [];
|
||||||
for (const link of externalLinks) {
|
for (const link of externalLinks) {
|
||||||
const href = await link.getAttribute('href');
|
const href = await link.getAttribute('href');
|
||||||
if (!href) continue;
|
if (!href) continue;
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 3;
|
||||||
|
let success = false;
|
||||||
|
while (attempts < maxAttempts && !success) {
|
||||||
|
attempts++;
|
||||||
try {
|
try {
|
||||||
const response = await request.head(href);
|
const response = await request.head(href, { timeout: 15000 });
|
||||||
if (response.status() >= 400) {
|
if (response.status() >= 400) {
|
||||||
brokenLinks.push({
|
brokenLinks.push({
|
||||||
href,
|
href,
|
||||||
status: response.status()
|
status: response.status(),
|
||||||
|
attempt: attempts
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
success = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (attempts === maxAttempts) {
|
||||||
brokenLinks.push({
|
brokenLinks.push({
|
||||||
href,
|
href,
|
||||||
error: error.message
|
error: error.message,
|
||||||
|
attempt: attempts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (brokenLinks.length > 0) {
|
if (brokenLinks.length > 0) {
|
||||||
console.log('\nBroken or inaccessible links:');
|
console.log('\nBroken or inaccessible links:');
|
||||||
brokenLinks.forEach(link => {
|
brokenLinks.forEach(link => {
|
||||||
if (link.error) {
|
if (link.error) {
|
||||||
console.log(`- ${link.href}: ${link.error}`);
|
console.log(`- ${link.href}: ${link.error} (Attempt ${link.attempt}/${maxAttempts})`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`- ${link.href}: HTTP ${link.status}`);
|
console.log(`- ${link.href}: HTTP ${link.status} (Attempt ${link.attempt}/${maxAttempts})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
throw new Error('Some external links are broken or inaccessible');
|
throw new Error('Some external links are broken or inaccessible');
|
||||||
|
@ -110,7 +137,7 @@ test.describe('Accessibility Tests', () => {
|
||||||
|
|
||||||
test('should have proper color contrast', async ({ page }) => {
|
test('should have proper color contrast', async ({ page }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running color contrast tests against ${url}`);
|
console.log(`Running color contrast tests against ${url} on ${viewport.name}`);
|
||||||
await page.goto(url, { timeout: 60000 });
|
await page.goto(url, { timeout: 60000 });
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
@ -156,7 +183,7 @@ test.describe('Accessibility Tests', () => {
|
||||||
|
|
||||||
test('should have alt text for all images', async ({ page }) => {
|
test('should have alt text for all images', async ({ page }) => {
|
||||||
const url = await getPageUrl(page);
|
const url = await getPageUrl(page);
|
||||||
console.log(`Running image alt text tests against ${url}`);
|
console.log(`Running image alt text tests against ${url} on ${viewport.name}`);
|
||||||
await page.goto(url, { timeout: 60000 });
|
await page.goto(url, { timeout: 60000 });
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
@ -192,3 +219,4 @@ test.describe('Accessibility Tests', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
|
@ -2,11 +2,12 @@ const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
test.describe('Security Headers Tests', () => {
|
test.describe('Security Headers Tests', () => {
|
||||||
test('should have all required security headers', async ({ page }) => {
|
test('should have all required security headers', async ({ page }) => {
|
||||||
// Navigate to the page
|
try {
|
||||||
await page.goto('http://localhost:8080');
|
// Navigate to the page with a timeout
|
||||||
|
await page.goto('http://localhost:8080', { timeout: 30000 });
|
||||||
|
|
||||||
// Get response headers
|
// Get response headers
|
||||||
const response = await page.waitForResponse('http://localhost:8080');
|
const response = await page.waitForResponse('http://localhost:8080', { timeout: 30000 });
|
||||||
const headers = response.headers();
|
const headers = response.headers();
|
||||||
|
|
||||||
// Define required headers and their expected values
|
// Define required headers and their expected values
|
||||||
|
@ -23,39 +24,58 @@ test.describe('Security Headers Tests', () => {
|
||||||
// Check each required header
|
// Check each required header
|
||||||
for (const [header, expectedValue] of Object.entries(requiredHeaders)) {
|
for (const [header, expectedValue] of Object.entries(requiredHeaders)) {
|
||||||
const headerValue = headers[header.toLowerCase()];
|
const headerValue = headers[header.toLowerCase()];
|
||||||
expect(headerValue).toBeDefined();
|
console.log(`Checking header: ${header}, Value: ${headerValue}`);
|
||||||
|
expect(headerValue, `${header} is not defined`).toBeDefined();
|
||||||
if (typeof expectedValue === 'string') {
|
if (typeof expectedValue === 'string') {
|
||||||
expect(headerValue).toBe(expectedValue);
|
expect(headerValue, `${header} does not match expected value`).toBe(expectedValue);
|
||||||
} else {
|
} else {
|
||||||
expect(headerValue).toMatch(expectedValue);
|
expect(headerValue, `${header} does not match expected pattern`).toMatch(expectedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in security headers test:', error);
|
||||||
|
throw new Error(`Failed to load local server for security headers test: ${error.message}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have correct CSP directives with nonce and hash', async ({ page }) => {
|
test('should have correct CSP directives with hash', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080');
|
try {
|
||||||
const response = await page.waitForResponse('http://localhost:8080');
|
await page.goto('http://localhost:8080', { timeout: 30000 });
|
||||||
|
const response = await page.waitForResponse('http://localhost:8080', { timeout: 30000 });
|
||||||
const headers = response.headers();
|
const headers = response.headers();
|
||||||
const csp = headers['content-security-policy'];
|
const csp = headers['content-security-policy'];
|
||||||
|
|
||||||
// Check for essential CSP directives
|
// Check for essential CSP directives
|
||||||
expect(csp).toContain("default-src 'self'");
|
console.log('CSP Header:', csp);
|
||||||
expect(csp).toContain("script-src 'self' 'nonce-");
|
expect(csp, 'CSP header is not defined').toBeDefined();
|
||||||
expect(csp).toContain("'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='");
|
expect(csp, 'CSP does not contain default-src directive').toContain("default-src 'none'");
|
||||||
expect(csp).toContain("style-src 'self' 'unsafe-inline'");
|
expect(csp, 'CSP does not contain script-src with hash').toContain("script-src 'self' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='");
|
||||||
expect(csp).toContain("img-src 'self' data: https: http:");
|
expect(csp, 'CSP does not contain style-src with hash').toContain("style-src 'self' 'sha256-Mo+7o3oPEKpX7fqRvTtunvQHlIDhJ0SxAMG1PCNniCI='");
|
||||||
expect(csp).toContain("font-src 'self'");
|
expect(csp, 'CSP does not contain img-src directive').toContain("img-src 'self' data:");
|
||||||
expect(csp).toContain("connect-src 'self'");
|
expect(csp, 'CSP does not contain font-src directive').toContain("font-src 'self' data:");
|
||||||
|
expect(csp, 'CSP does not contain connect-src directive').toContain("connect-src 'self'");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in CSP directives test:', error);
|
||||||
|
throw new Error(`Failed to load local server for CSP test: ${error.message}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have nonce attributes on script tags', async ({ page }) => {
|
test('should have integrity attributes on script tags', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080');
|
try {
|
||||||
|
await page.goto('http://localhost:8080', { timeout: 30000 });
|
||||||
|
|
||||||
// Check that all script tags have nonce attributes
|
// Check that all script tags have integrity attributes
|
||||||
const scripts = await page.$$('script');
|
const scripts = await page.$$('script');
|
||||||
|
console.log(`Found ${scripts.length} script tags`);
|
||||||
for (const script of scripts) {
|
for (const script of scripts) {
|
||||||
const hasNonce = await script.evaluate(el => el.hasAttribute('nonce'));
|
const hasIntegrity = await script.evaluate(el => el.hasAttribute('integrity'));
|
||||||
expect(hasNonce).toBeTruthy();
|
const src = await script.evaluate(el => el.getAttribute('src') || 'inline script');
|
||||||
|
console.log(`Script ${src} has integrity: ${hasIntegrity}`);
|
||||||
|
expect(hasIntegrity, `Script tag ${src} missing integrity attribute`).toBeTruthy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in integrity attributes test:', error);
|
||||||
|
throw new Error(`Failed to load local server for integrity test: ${error.message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
Loading…
Reference in New Issue