mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-17 14:04:24 +02:00
Finished Iframe Implementation
This commit is contained in:
parent
a100c9e63d
commit
9159a0c7d3
30
filter_plugins/csp_hashes.py
Normal file
30
filter_plugins/csp_hashes.py
Normal file
@ -0,0 +1,30 @@
|
||||
from ansible.errors import AnsibleFilterError
|
||||
import copy
|
||||
|
||||
def append_csp_hash(applications, application_id, code_one_liner):
|
||||
"""
|
||||
Ensures that applications[application_id].csp.hashes['script-src-elem']
|
||||
exists and appends the given one-liner (if not already present).
|
||||
"""
|
||||
if not isinstance(applications, dict):
|
||||
raise AnsibleFilterError("`applications` must be a dict")
|
||||
if application_id not in applications:
|
||||
raise AnsibleFilterError(f"Unknown application_id: {application_id}")
|
||||
|
||||
apps = copy.deepcopy(applications)
|
||||
app = apps[application_id]
|
||||
csp = app.setdefault('csp', {})
|
||||
hashes = csp.setdefault('hashes', {})
|
||||
|
||||
existing = hashes.get('script-src-elem', [])
|
||||
if code_one_liner not in existing:
|
||||
existing.append(code_one_liner)
|
||||
hashes['script-src-elem'] = existing
|
||||
|
||||
return apps
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
'append_csp_hash': append_csp_hash
|
||||
}
|
44
filter_plugins/text_filters.py
Normal file
44
filter_plugins/text_filters.py
Normal file
@ -0,0 +1,44 @@
|
||||
# filter_plugins/text_filters.py
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
import re
|
||||
|
||||
def to_one_liner(s):
|
||||
"""
|
||||
Collapse any multi-line string into a single line,
|
||||
trim extra whitespace, and remove JavaScript comments.
|
||||
Supports removal of both '//' line comments and '/*...*/' block comments,
|
||||
but preserves '//' inside string literals and templating expressions.
|
||||
"""
|
||||
if not isinstance(s, str):
|
||||
raise AnsibleFilterError("to_one_liner() expects a string")
|
||||
|
||||
# 1) Remove block comments /* ... */
|
||||
no_block_comments = re.sub(r'/\*.*?\*/', '', s, flags=re.DOTALL)
|
||||
|
||||
# 2) Extract string literals to protect them from comment removal
|
||||
string_pattern = re.compile(r"'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"")
|
||||
literals = []
|
||||
def _extract(match):
|
||||
idx = len(literals)
|
||||
literals.append(match.group(0))
|
||||
return f"__STR{idx}__"
|
||||
temp = string_pattern.sub(_extract, no_block_comments)
|
||||
|
||||
# 3) Remove line comments // ...
|
||||
temp = re.sub(r'//.*$', '', temp, flags=re.MULTILINE)
|
||||
|
||||
# 4) Restore string literals
|
||||
for idx, lit in enumerate(literals):
|
||||
temp = temp.replace(f"__STR{idx}__", lit)
|
||||
|
||||
# 5) Collapse all whitespace
|
||||
one_liner = re.sub(r'\s+', ' ', temp).strip()
|
||||
|
||||
return one_liner
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
'to_one_liner': to_one_liner,
|
||||
}
|
@ -6,7 +6,7 @@ setup_admin_email: "{{ users.administrator.email }}"
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
credentials:
|
||||
domains:
|
||||
|
@ -5,7 +5,7 @@ credentials:
|
||||
features:
|
||||
matomo: true
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
portfolio_iframe: true
|
||||
central_database: true
|
||||
docker:
|
||||
services:
|
||||
|
@ -6,8 +6,8 @@ accounts:
|
||||
icon:
|
||||
class: fa-solid fa-users
|
||||
children:
|
||||
- name: Publishing Channels
|
||||
description: Platforms where I share content.
|
||||
- name: Follow Us
|
||||
description: Follow us to stay up to recieve the newest CyMaIS updates
|
||||
icon:
|
||||
class: fas fa-newspaper
|
||||
{% if ["mastodon", "bluesky"] | any_in(group_names) %}
|
||||
@ -32,7 +32,7 @@ accounts:
|
||||
icon:
|
||||
class: fa-brands fa-bluesky
|
||||
alternatives:
|
||||
- link: accounts.publishingchannels.microblogs.mastodon
|
||||
- link: accounts.followus.microblogs.mastodon
|
||||
identifier: "{{service_provider.contact.bluesky}}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@ -102,7 +102,6 @@ company:
|
||||
navigation:
|
||||
header:
|
||||
children:
|
||||
- link: accounts.publishingchannels
|
||||
- name: Contact
|
||||
description: Get in touch with {{ 'us' if service_provider.type == 'legal' else 'me' }}
|
||||
icon:
|
||||
@ -146,4 +145,10 @@ navigation:
|
||||
class: fa-solid fa-expand-arrows-alt
|
||||
onclick: "toggleFullscreen()"
|
||||
|
||||
- name: Open in new tab
|
||||
description: Open the currently embedded iframe URL in a fresh browser tab
|
||||
icon:
|
||||
class: fa-solid fa-up-right-from-square
|
||||
onclick: openIframeInNewTab()
|
||||
|
||||
{% include 'footer_menu.yaml.j2' %}
|
30
roles/docker-portfolio/templates/javascript.js.j2
Normal file
30
roles/docker-portfolio/templates/javascript.js.j2
Normal file
@ -0,0 +1,30 @@
|
||||
window.addEventListener("message", function(event) {
|
||||
const allowedSuffix = ".{{ primary_domain }}";
|
||||
const origin = event.origin;
|
||||
|
||||
// 1. Only allow messages from *.{{ primary_domain }}
|
||||
if (!origin.endsWith(allowedSuffix)) return;
|
||||
|
||||
const data = event.data;
|
||||
|
||||
// 2. Only process valid iframeLocationChange messages
|
||||
if (data && data.type === "iframeLocationChange" && typeof data.href === "string") {
|
||||
try {
|
||||
const hrefUrl = new URL(data.href);
|
||||
|
||||
// 3. Only allow redirects to *.{{ primary_domain }}
|
||||
if (!hrefUrl.hostname.endsWith(allowedSuffix)) return;
|
||||
|
||||
// 4. Update the ?iframe= parameter in the browser URL
|
||||
const newUrl = new URL(window.location);
|
||||
newUrl.searchParams.set("iframe", hrefUrl.href);
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
} catch (e) {
|
||||
// Invalid or malformed URL – ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("[iframe-sync] Listener for iframe messages is active.");
|
||||
{% endif %}
|
@ -3,7 +3,7 @@ features:
|
||||
css: true
|
||||
portfolio_iframe: false
|
||||
simpleicons: true # Activate Brand Icons for your groups
|
||||
nasa_api_key: false # Set api key to use the Nasa Picture of the Day as Background
|
||||
javascript: true # Necessary for URL sync
|
||||
csp:
|
||||
whitelist:
|
||||
script-src-elem:
|
||||
|
@ -6,4 +6,14 @@
|
||||
- name: "Activate Global Matomo Tracking for {{domain}}"
|
||||
include_role:
|
||||
name: nginx-modifier-matomo
|
||||
when: applications | is_feature_enabled('matomo',application_id)
|
||||
when: applications | is_feature_enabled('matomo',application_id)
|
||||
|
||||
- name: "Activate Portfolio iFrame Notifier for {{ domain }}"
|
||||
include_role:
|
||||
name: nginx-modifier-iframe
|
||||
when: applications | is_feature_enabled('portfolio_iframe', application_id)
|
||||
|
||||
- name: "Activate Javascript for {{ domain }}"
|
||||
include_role:
|
||||
name: nginx-modifier-javascript
|
||||
when: applications | is_feature_enabled('javascript', application_id)
|
@ -2,20 +2,32 @@
|
||||
sub_filter_once off;
|
||||
sub_filter_types text/html;
|
||||
|
||||
{% set features_css_final = applications.get(application_id).get('features').get('css') | bool %}
|
||||
{% set features_matomo_final = applications.get(application_id).get('features').get('matomo') | bool %}
|
||||
{% set modifier_css_enabled = applications | is_feature_enabled('css',application_id) %}
|
||||
{% set modifier_matomo_enabled = applications | is_feature_enabled('matomo',application_id) %}
|
||||
{% set modifier_iframe_enabled = applications | is_feature_enabled('portfolio_iframe',application_id) %}
|
||||
{% set modifier_javascript_enabled = applications | is_feature_enabled('javascript',application_id) %}
|
||||
|
||||
|
||||
{% if features_matomo_final | bool %}
|
||||
{# Include Global Matomo Tracking #}
|
||||
{% include 'roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2' %}
|
||||
{% if modifier_iframe_enabled or modifier_css_enabled or modifier_matomo_enabled or modifier_javascript_enabled %}
|
||||
sub_filter '</head>' '
|
||||
{%- if modifier_css_enabled -%}
|
||||
{%- include "roles/nginx-modifier-css/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_matomo_enabled -%}
|
||||
{%- include "roles/nginx-modifier-matomo/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_iframe_enabled -%}
|
||||
{%- include "roles/nginx-modifier-iframe/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
{%- if modifier_javascript_enabled -%}
|
||||
{%- include "roles/nginx-modifier-javascript/templates/head_sub.j2" -%}
|
||||
{%- endif -%}
|
||||
</head>';
|
||||
{% endif %}
|
||||
|
||||
{% if features_css_final | bool or features_matomo_final | bool %}
|
||||
sub_filter '</head>' '{% if features_matomo_final | bool %}{% include 'roles/nginx-modifier-matomo/templates/script.j2' %}{% endif %}{% if features_css_final | bool %}{% include 'roles/nginx-modifier-css/templates/link.j2' %}{% endif %}</head>';
|
||||
{% if modifier_css_enabled | bool %}
|
||||
{% include 'roles/nginx-modifier-css/templates/location.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if features_css_final | bool %}
|
||||
{# Include Global CSS Location #}
|
||||
{% include 'roles/nginx-modifier-css/templates/location.conf.j2' %}
|
||||
{% endif %}
|
||||
{% if modifier_matomo_enabled %}
|
||||
{% include 'roles/nginx-modifier-matomo/templates/matomo-tracking.conf.j2' %}
|
||||
{% endif %}
|
24
roles/nginx-modifier-iframe/README.md
Normal file
24
roles/nginx-modifier-iframe/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
# 🌐 iFrame Notifier for Nginx
|
||||
|
||||
This Ansible role injects a small JavaScript snippet into your HTML responses that enables parent pages to get notified whenever the iframe’s location changes and forces external links to open in a new tab.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Location Change Notification**
|
||||
Uses `postMessage` to inform the parent window of any URL changes inside the iframe (including pushState/popState events) for seamless SPA support.
|
||||
|
||||
- **External Link Handling**
|
||||
Automatically sets `target="_blank"` and `rel="noopener"` on links pointing outside your primary domain to improve security and user experience.
|
||||
|
||||
- **Easy CSP Integration**
|
||||
Calculates a CSP hash for the injected script so you can safely allow it via your Content Security Policy.
|
||||
|
||||
---
|
||||
|
||||
## Author
|
||||
|
||||
Developed by **Kevin Veen-Birkenbach**
|
||||
[https://www.veen.world](https://www.veen.world) 🎉
|
28
roles/nginx-modifier-iframe/meta/main.yml
Normal file
28
roles/nginx-modifier-iframe/meta/main.yml
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Injects a JS snippet into HTML to notify parent windows of iframe location changes and force external links to new tabs."
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
repository: https://s.veen.world/cymais
|
||||
issue_tracker_url: https://s.veen.world/cymaisissues
|
||||
documentation: https://s.veen.world/cymais
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- iframe
|
||||
- javascript
|
||||
- csp
|
||||
- security
|
||||
- postMessage
|
||||
dependencies:
|
||||
- nginx
|
12
roles/nginx-modifier-iframe/tasks/main.yml
Normal file
12
roles/nginx-modifier-iframe/tasks/main.yml
Normal file
@ -0,0 +1,12 @@
|
||||
- name: Load iFrame handler JS template
|
||||
set_fact:
|
||||
iframe_code: "{{ lookup('template','iframe-handler.js.j2') }}"
|
||||
|
||||
- name: Collapse iFrame code into one-liner
|
||||
set_fact:
|
||||
iframe_code_one_liner: "{{ iframe_code | to_one_liner }}"
|
||||
|
||||
- name: Append iFrame CSP hash
|
||||
set_fact:
|
||||
applications: "{{ applications | append_csp_hash(application_id, iframe_code_one_liner) }}"
|
||||
changed_when: false
|
1
roles/nginx-modifier-iframe/templates/head_sub.j2
Normal file
1
roles/nginx-modifier-iframe/templates/head_sub.j2
Normal file
@ -0,0 +1 @@
|
||||
<script>{{ iframe_code_one_liner | replace("'", "\\'") }}</script>
|
46
roles/nginx-modifier-iframe/templates/iframe-handler.js.j2
Normal file
46
roles/nginx-modifier-iframe/templates/iframe-handler.js.j2
Normal file
@ -0,0 +1,46 @@
|
||||
(function() {
|
||||
var primary = "{{ primary_domain }}";
|
||||
var allowedOrigin = "https://{{ domains | get_domain('portfolio') }}";
|
||||
|
||||
function notifyParent() {
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: "iframeLocationChange",
|
||||
href: window.location.href
|
||||
}, allowedOrigin);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function forceExternalLinks() {
|
||||
Array.prototype.forEach.call(document.querySelectorAll("a[href]"), function(a) {
|
||||
try {
|
||||
var url = new URL(a.href, location);
|
||||
if (!url.hostname.endsWith(primary)) {
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("load", function() {
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
});
|
||||
window.addEventListener("popstate", function() {
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
});
|
||||
|
||||
// SPA support
|
||||
var _pushState = history.pushState;
|
||||
history.pushState = function() {
|
||||
_pushState.apply(history, arguments);
|
||||
notifyParent();
|
||||
forceExternalLinks();
|
||||
};
|
||||
})();
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("[iframe-sync] Sender for iframe messages is active.");
|
||||
{% endif %}
|
28
roles/nginx-modifier-javascript/README.md
Normal file
28
roles/nginx-modifier-javascript/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# 🌐 Global JavaScript Injector for Nginx
|
||||
|
||||
## Description
|
||||
|
||||
This Ansible role injects a custom JavaScript snippet into all HTML responses served by Nginx. It leverages Nginx’s `sub_filter` to seamlessly insert your application-specific script just before the closing `</head>` tag, ensuring that your code runs on every page load—perfect for global feature flags, analytics, or UI enhancements.
|
||||
|
||||
## Features
|
||||
|
||||
- **One-line Script Injection**
|
||||
Collapses your JavaScript into a single line and injects it via `sub_filter` for minimal footprint and maximal compatibility.
|
||||
|
||||
- **Easy CSP Integration**
|
||||
Automatically computes and appends a CSP hash entry for your script, so you can lock down Content Security Policy without lifting a finger.
|
||||
|
||||
- **Conditional Activation**
|
||||
Activates only when you enable the `javascript` feature for a given application, keeping your server blocks clean and performant.
|
||||
|
||||
- **Debug Mode**
|
||||
Supports an `enable_debug` flag that appends optional `console.log` statements for easier troubleshooting in staging or development.
|
||||
|
||||
## Author
|
||||
|
||||
Developed by **Kevin Veen-Birkenbach**
|
||||
Consulting & Coaching Solutions — [veen.world](https://www.veen.world)
|
||||
|
||||
---
|
||||
|
||||
Happy automating! 🎉
|
28
roles/nginx-modifier-javascript/meta/main.yml
Normal file
28
roles/nginx-modifier-javascript/meta/main.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Injects a custom JavaScript snippet into Nginx-served HTML responses via sub_filter."
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- nginx
|
||||
- javascript
|
||||
- csp
|
||||
- sub_filter
|
||||
- injection
|
||||
- global
|
||||
repository: "https://s.veen.world/cymais"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
|
||||
dependencies:
|
||||
- nginx
|
12
roles/nginx-modifier-javascript/tasks/main.yml
Normal file
12
roles/nginx-modifier-javascript/tasks/main.yml
Normal file
@ -0,0 +1,12 @@
|
||||
- name: "Load JavaScript code for '{{ application_id }}'"
|
||||
set_fact:
|
||||
javascript_code: "{{ lookup('template', modifier_javascript_template_file) }}"
|
||||
|
||||
- name: "Collapse Javascript code into one-liner for '{{application_id}}'"
|
||||
set_fact:
|
||||
javascript_code_one_liner: "{{ javascript_code | to_one_liner }}"
|
||||
|
||||
- name: "Append Javascript CSP hash for '{{application_id}}'"
|
||||
set_fact:
|
||||
applications: "{{ applications | append_csp_hash(application_id, javascript_code_one_liner) }}"
|
||||
changed_when: false
|
1
roles/nginx-modifier-javascript/templates/head_sub.j2
Normal file
1
roles/nginx-modifier-javascript/templates/head_sub.j2
Normal file
@ -0,0 +1 @@
|
||||
<script>{{ javascript_code_one_liner | replace("'", "\\'") }}</script>
|
1
roles/nginx-modifier-javascript/vars/main.yml
Normal file
1
roles/nginx-modifier-javascript/vars/main.yml
Normal file
@ -0,0 +1 @@
|
||||
modifier_javascript_template_file: "{{ playbook_dir }}/roles/docker-{{ application_id }}/templates/javascript.js.j2"
|
@ -45,45 +45,16 @@
|
||||
when: "matomo_site_id is not defined or matomo_site_id is none"
|
||||
changed_when: false
|
||||
|
||||
- name: Set the Matomo tracking code from a template file
|
||||
- name: Load Matomo tracking JS template
|
||||
set_fact:
|
||||
matomo_tracking_code: "{{ lookup('template', 'matomo-tracking.js.j2') }}"
|
||||
matomo_tracking_code: "{{ lookup('template','matomo-tracking.js.j2') }}"
|
||||
|
||||
- name: Set the tracking code as a one-liner
|
||||
- name: Collapse Matomo code into one-liner
|
||||
set_fact:
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | regex_replace('\\n', '') | regex_replace('\\s+', ' ') }}"
|
||||
matomo_tracking_code_one_liner: "{{ matomo_tracking_code | to_one_liner }}"
|
||||
|
||||
- name: Ensure csp.hashes exists for this app
|
||||
- name: Append Matomo CSP hash
|
||||
set_fact:
|
||||
applications: >-
|
||||
{{
|
||||
applications
|
||||
| combine({
|
||||
(application_id): {
|
||||
'csp': {
|
||||
'hashes': {}
|
||||
}
|
||||
}
|
||||
}, recursive=True)
|
||||
}}
|
||||
applications: "{{ applications | append_csp_hash(application_id, matomo_tracking_code_one_liner) }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Append Matomo one-liner to script-src inline hashes
|
||||
set_fact:
|
||||
applications: >-
|
||||
{{
|
||||
applications
|
||||
| combine({
|
||||
(application_id): {
|
||||
'csp': {
|
||||
'hashes': {
|
||||
'script-src-elem': (
|
||||
applications[application_id]['csp']['hashes'].get('script-src', [])
|
||||
+ [ matomo_tracking_code_one_liner ]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, recursive=True)
|
||||
}}
|
||||
changed_when: false
|
||||
|
@ -13,3 +13,7 @@ _paq.push(["enableLinkTracking"]);
|
||||
var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0];
|
||||
g.async=true; g.src=u+"matomo.js"; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
|
||||
{% if enable_debug | bool %}
|
||||
console.log("Matomo is loaded.");
|
||||
{% endif %}
|
1
templates/docker_role/templates/javascript.js.j2
Normal file
1
templates/docker_role/templates/javascript.js.j2
Normal file
@ -0,0 +1 @@
|
||||
alert('Custom JS loaded');
|
@ -16,6 +16,7 @@ features:
|
||||
central_database: false # Enable Central Database Network
|
||||
recaptcha: false # Enable ReCaptcha
|
||||
oauth2: false # Enable the OAuth2-Proy
|
||||
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
||||
csp:
|
||||
whitelist: {} # URL's which should be whitelisted
|
||||
flags: {} # Flags which should be set
|
||||
|
52
tests/unit/filter_plugins/test_csp_hashes.py
Normal file
52
tests/unit/filter_plugins/test_csp_hashes.py
Normal file
@ -0,0 +1,52 @@
|
||||
# tests/unit/filter_plugins/test_csp_hashes.py
|
||||
import unittest
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from filter_plugins.csp_hashes import append_csp_hash
|
||||
|
||||
class TestCspHashes(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Sample applications dict for testing
|
||||
self.applications = {
|
||||
'app1': {
|
||||
'csp': {
|
||||
'hashes': {
|
||||
'script-src-elem': ["existing-hash"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.code = "new-hash" # example one-liner hash
|
||||
|
||||
def test_appends_new_hash(self):
|
||||
result = append_csp_hash(self.applications, 'app1', self.code)
|
||||
# Original remains unchanged
|
||||
self.assertNotIn(self.code, self.applications['app1']['csp']['hashes']['script-src-elem'])
|
||||
# New result should contain both existing and new
|
||||
self.assertIn('existing-hash', result['app1']['csp']['hashes']['script-src-elem'])
|
||||
self.assertIn(self.code, result['app1']['csp']['hashes']['script-src-elem'])
|
||||
|
||||
def test_does_not_duplicate_existing_hash(self):
|
||||
# Append an existing hash
|
||||
result = append_csp_hash(self.applications, 'app1', 'existing-hash')
|
||||
# Should still only have one instance
|
||||
hashes = result['app1']['csp']['hashes']['script-src-elem']
|
||||
self.assertEqual(hashes.count('existing-hash'), 1)
|
||||
|
||||
def test_creates_missing_csp_structure(self):
|
||||
# Remove csp and hashes keys
|
||||
apps = {'app2': {}}
|
||||
result = append_csp_hash(apps, 'app2', self.code)
|
||||
self.assertIn('csp', result['app2'])
|
||||
self.assertIn('hashes', result['app2']['csp'])
|
||||
self.assertIn(self.code, result['app2']['csp']['hashes']['script-src-elem'])
|
||||
|
||||
def test_non_dict_applications_raises(self):
|
||||
with self.assertRaises(AnsibleFilterError):
|
||||
append_csp_hash('not-a-dict', 'app1', self.code)
|
||||
|
||||
def test_unknown_application_id_raises(self):
|
||||
with self.assertRaises(AnsibleFilterError):
|
||||
append_csp_hash(self.applications, 'unknown', self.code)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
64
tests/unit/filter_plugins/test_text_filters.py
Normal file
64
tests/unit/filter_plugins/test_text_filters.py
Normal file
@ -0,0 +1,64 @@
|
||||
# tests/unit/filter_plugins/test_text_filters.py
|
||||
import unittest
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from filter_plugins.text_filters import to_one_liner
|
||||
|
||||
class TestTextFilters(unittest.TestCase):
|
||||
def test_collapse_whitespace(self):
|
||||
s = """Line one
|
||||
|
||||
Line two
|
||||
Line three"""
|
||||
expected = "Line one Line two Line three"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_remove_js_line_comments(self):
|
||||
s = "var a = 1; // this is a comment\nvar b = 2;"
|
||||
expected = "var a = 1; var b = 2;"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_remove_js_block_comments(self):
|
||||
s = "var a = /* comment inside */ 1; var b = 2;"
|
||||
expected = "var a = 1; var b = 2;"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_remove_multiple_comments(self):
|
||||
s = "// first comment\nvar a = 1; /* block comment */ var b = 2; // end comment"
|
||||
expected = "var a = 1; var b = 2;"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_strips_leading_trailing_whitespace(self):
|
||||
s = " \n some text here \n "
|
||||
expected = "some text here"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_non_string_raises(self):
|
||||
with self.assertRaises(AnsibleFilterError):
|
||||
to_one_liner(123)
|
||||
|
||||
def test_preserve_urls_in_string_literals(self):
|
||||
s = 'var url = "http://example.com/path"; // comment'
|
||||
expected = 'var url = "http://example.com/path";'
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_preserve_escaped_quotes_and_protocol(self):
|
||||
s = "var s = 'He said \\'//not comment\\''; // remove this"
|
||||
expected = "var s = 'He said \\'//not comment\\'';"
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_preserve_templating_expressions(self):
|
||||
s = 'var tracker = "//{{ domains | get_domain(\'matomo\') }}/matomo.js"; // loader'
|
||||
expected = 'var tracker = "//{{ domains | get_domain(\'matomo\') }}/matomo.js";'
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
def test_mixed_string_and_comment(self):
|
||||
s = '''
|
||||
var a = "foo // still part of string";
|
||||
// top-level comment
|
||||
var b = 2; // end comment
|
||||
'''
|
||||
expected = 'var a = "foo // still part of string"; var b = 2;'
|
||||
self.assertEqual(to_one_liner(s), expected)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user