mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-08 03:07:14 +02:00
Compare commits
8 Commits
ff18c7cd73
...
c8be88e3b1
Author | SHA1 | Date | |
---|---|---|---|
c8be88e3b1 | |||
5e315f9603 | |||
bab1035a24 | |||
30930c4136 | |||
bba663f95d | |||
c2f83abb60 | |||
3bc64023af | |||
d94254effb |
@@ -67,7 +67,7 @@ _applications_nextcloud_oidc_flavor: >-
|
||||
False,
|
||||
'oidc_login'
|
||||
if applications
|
||||
| get_app_conf('web-app-nextcloud','features.ldap',False)
|
||||
| get_app_conf('web-app-nextcloud','features.ldap',False, True)
|
||||
else 'sociallogin'
|
||||
)
|
||||
}}
|
@@ -5,7 +5,7 @@ services:
|
||||
{% include 'roles/cmp-rdbms/templates/services/main.yml.j2' %}
|
||||
{% endif %}
|
||||
{# Load Redis #}
|
||||
{% if applications | is_docker_service_enabled(application_id, 'redis') %}
|
||||
{% if applications | is_docker_service_enabled(application_id, 'redis') or applications | get_app_conf(application_id, 'features.oauth2', False) %}
|
||||
{% include 'roles/svc-db-redis/templates/service.yml.j2' %}
|
||||
{% endif %}
|
||||
{# Load OAuth2 Proxy #}
|
||||
|
@@ -1,7 +1,11 @@
|
||||
{# This template needs to be included in docker-compose.yml which contain a database and additional volumes #}
|
||||
volumes:
|
||||
{% if not applications | get_app_conf(application_id, 'features.central_database', False) and applications | get_app_conf(application_id, 'docker.services.database.enabled', False) %}
|
||||
{% if applications | is_docker_service_enabled(application_id, 'database') and not applications | get_app_conf(application_id, 'features.central_database', False) %}
|
||||
database:
|
||||
name: {{ database_volume }}
|
||||
{% endif %}
|
||||
{% if applications | is_docker_service_enabled(application_id, 'redis') or applications | get_app_conf(application_id, 'features.oauth2', False) %}
|
||||
redis:
|
||||
name: {{ application_id | get_entity_name }}
|
||||
{% endif %}
|
||||
{{ "\n" }}
|
@@ -26,6 +26,10 @@ server {
|
||||
|
||||
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.j2' %}
|
||||
|
||||
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) or domain == primary_domain %}
|
||||
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if ws_path is defined %}
|
||||
location {{ ws_path }} {
|
||||
proxy_set_header Host $host;
|
||||
|
28
roles/srv-web-7-7-inj-logout/meta/main.yml
Normal file
28
roles/srv-web-7-7-inj-logout/meta/main.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Injects a catcher, which catches the actions of all logout elements and redirects them to the central logout."
|
||||
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:
|
||||
- srv-web-7-4-core
|
13
roles/srv-web-7-7-inj-logout/tasks/main.yml
Normal file
13
roles/srv-web-7-7-inj-logout/tasks/main.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
# run_once_srv_web_7_7_inj_javascript: deactivated
|
||||
- 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/srv-web-7-7-inj-logout/templates/head_sub.j2
Normal file
1
roles/srv-web-7-7-inj-logout/templates/head_sub.j2
Normal file
@@ -0,0 +1 @@
|
||||
<script>{{ javascript_code_one_liner | replace("'", "\\'") }}</script>
|
38
roles/srv-web-7-7-inj-logout/templates/logout.js.j2
Normal file
38
roles/srv-web-7-7-inj-logout/templates/logout.js.j2
Normal file
@@ -0,0 +1,38 @@
|
||||
(function() {
|
||||
const logoutUrlBase = 'https://auth.cymais.cloud/realms/cymais.cloud/protocol/openid-connect/logout';
|
||||
const redirectUri = encodeURIComponent('https://cymais.cloud');
|
||||
const logoutUrl = `${logoutUrlBase}?redirect_uri=${redirectUri}`;
|
||||
|
||||
// Check if a string matches logout keywords
|
||||
function matchesLogout(str) {
|
||||
return str && /logout|log\s*out|abmelden/i.test(str);
|
||||
}
|
||||
|
||||
// Check if any attribute name contains "logout" (case-insensitive)
|
||||
function hasLogoutAttribute(el) {
|
||||
for (let attr of el.attributes) {
|
||||
if (/logout/i.test(attr.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find all elements
|
||||
const allElements = document.querySelectorAll('*');
|
||||
allElements.forEach(el => {
|
||||
if (
|
||||
matchesLogout(el.getAttribute('name')) ||
|
||||
matchesLogout(el.id) ||
|
||||
matchesLogout(el.className) ||
|
||||
matchesLogout(el.innerText) ||
|
||||
hasLogoutAttribute(el)
|
||||
) {
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
window.location.href = logoutUrl;
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
1
roles/srv-web-7-7-inj-logout/vars/main.yml
Normal file
1
roles/srv-web-7-7-inj-logout/vars/main.yml
Normal file
@@ -0,0 +1 @@
|
||||
modifier_javascript_template_file: "{{ application_id | abs_role_path_by_application_id }}/templates/javascript.js.j2"
|
@@ -50,6 +50,6 @@
|
||||
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
||||
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
|
||||
listen:
|
||||
- "Import data LDIF files"
|
||||
- "Import groups LDIF files"
|
||||
- "Import all LDIF files"
|
||||
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/groups/*.j2') | sort }}"
|
@@ -24,6 +24,5 @@
|
||||
- .:/var/www
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
@@ -18,3 +18,7 @@ docker:
|
||||
name: "baserow"
|
||||
volumes:
|
||||
data: "baserow_data"
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- baserow.{{ primary_domain }}
|
||||
|
@@ -14,6 +14,5 @@
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ baserow_volume }}
|
||||
redis:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
@@ -1,7 +1,6 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
{{ discourse_network }}:
|
||||
|
@@ -1,2 +1,6 @@
|
||||
features:
|
||||
universal_logout: false # Just deactivated to oppress warnings, elk is anyhow not running
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- elk.{{ primary_domain }}
|
||||
|
@@ -24,6 +24,8 @@ csp:
|
||||
domains:
|
||||
aliases:
|
||||
- "crm.{{ primary_domain }}"
|
||||
canonical:
|
||||
- espocrm.{{ primary_domain }}
|
||||
email:
|
||||
from_name: "Customer Relationship Management ({{ primary_domain }})"
|
||||
docker:
|
||||
|
@@ -63,7 +63,6 @@
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
funkwhale_static_root:
|
||||
redis:
|
||||
music:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
@@ -36,6 +36,8 @@ csp:
|
||||
domains:
|
||||
aliases:
|
||||
- "git.{{ primary_domain }}"
|
||||
canonical:
|
||||
- gitea.{{ primary_domain }}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
@@ -15,3 +15,7 @@ docker:
|
||||
version: "latest"
|
||||
credentials:
|
||||
initial_root_password: "{{ users.administrator.password }}"
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- gitlab.{{ primary_domain }}
|
||||
|
@@ -16,7 +16,6 @@
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
config:
|
||||
logs:
|
||||
data:
|
||||
|
@@ -1,2 +1,6 @@
|
||||
features:
|
||||
universal_logout: true # Same like with elk, anyhow not active atm
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- jenkins.{{ primary_domain }}
|
||||
|
@@ -11,7 +11,7 @@ features:
|
||||
port-ui-desktop: true
|
||||
ldap: true
|
||||
central_database: false
|
||||
oauth2: false
|
||||
oauth2: true
|
||||
universal_logout: true
|
||||
csp:
|
||||
flags:
|
||||
@@ -25,4 +25,6 @@ csp:
|
||||
domains:
|
||||
aliases:
|
||||
- "ldap.{{primary_domain}}"
|
||||
canonical:
|
||||
- lam.{{ primary_domain }}
|
||||
|
||||
|
@@ -22,7 +22,8 @@ csp:
|
||||
whitelist: {} # URL's which should be whitelisted
|
||||
flags: {} # Flags which should be set
|
||||
domains:
|
||||
canonical: {} # Urls under which the domain should be directly accessible
|
||||
canonical:
|
||||
- "libretranslate.{{ primary_domain }}"
|
||||
aliases: [] # Alias redirections to the first element of the canonical domains
|
||||
rbac:
|
||||
roles: {}
|
||||
|
@@ -100,7 +100,7 @@
|
||||
- "{{docker_compose.directories.volumes}}overrides/rspamd:/overrides:ro"
|
||||
depends_on:
|
||||
- front
|
||||
- redis
|
||||
- redis
|
||||
- antivirus
|
||||
- resolver
|
||||
dns:
|
||||
@@ -181,8 +181,6 @@
|
||||
name: {{ mailu_dkim }}
|
||||
dovecot_mail:
|
||||
name: {{ mailu_dovecot_mail }}
|
||||
redis:
|
||||
name: {{ mailu_redis }}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
radicale:
|
||||
|
@@ -44,7 +44,6 @@
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
data:
|
||||
name: "{{ mastodon_volume }}"
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
@@ -13,4 +13,9 @@ server {
|
||||
|
||||
{% include 'roles/srv-web-7-7-inj-compose/templates/global.includes.conf.j2'%}
|
||||
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.j2' %}
|
||||
|
||||
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
|
||||
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
}
|
@@ -15,3 +15,7 @@ docker:
|
||||
name: "mybb"
|
||||
volumes:
|
||||
data: "mybb_data"
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- mybb.{{ primary_domain }}
|
||||
|
@@ -71,6 +71,5 @@
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: {{ nextcloud_volume }}
|
||||
redis:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
|
@@ -20,6 +20,10 @@ server
|
||||
|
||||
{% include 'roles/srv-proxy-7-4-core/templates/location/proxy_basic.conf.j2' %}
|
||||
|
||||
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
|
||||
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
location ^~ /.well-known {
|
||||
rewrite ^/\.well-known/host-meta\.json /public.php?service=host-meta-json last;
|
||||
rewrite ^/\.well-known/host-meta /public.php?service=host-meta last;
|
||||
|
@@ -42,7 +42,7 @@ nextcloud_cron_name: "{{ applications | get_app_conf(
|
||||
nextcloud_talk_name: "{{ applications | get_app_conf(application_id, 'docker.services.talk.name', True) }}"
|
||||
nextcloud_talk_image: "{{ applications | get_app_conf(application_id, 'docker.services.talk.image', True) }}"
|
||||
nextcloud_talk_version: "{{ applications | get_app_conf(application_id, 'docker.services.talk.version', True) }}"
|
||||
nextcloud_talk_enabled: "{{ applications | get_app_conf(application_id, 'docker.services.talk.enabled', True) }}"
|
||||
nextcloud_talk_enabled: "{{ applications | is_docker_service_enabled(application_id, 'talk') }}"
|
||||
nextcloud_talk_stun_port: "{{ ports.public.stun[application_id] }}"
|
||||
# nextcloud_talk_domain: "{{ domains[application_id].talk }}"
|
||||
|
||||
|
@@ -6,3 +6,7 @@ features:
|
||||
css: true
|
||||
port-ui-desktop: false
|
||||
universal_logout: true
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- oauth2-proxy.{{ primary_domain }}
|
||||
|
@@ -21,4 +21,7 @@ allowed_groups = {{ applications | get_app_conf(oauth2_proxy_applicat
|
||||
email_domains = ["*"]
|
||||
{% else %}
|
||||
email_domains = "{{ primary_domain }}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
session_store_type = "redis"
|
||||
redis_connection_url = "redis://redis:6379"
|
@@ -19,7 +19,6 @@
|
||||
assets:
|
||||
data:
|
||||
name: {{ peertube_volume }}
|
||||
redis:
|
||||
config:
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
@@ -27,6 +27,10 @@ server {
|
||||
proxy_pass http://127.0.0.1:{{ports.localhost.http[application_id]}};
|
||||
}
|
||||
|
||||
{% if applications | get_app_conf(application_id, 'features.universal_logout', False) %}
|
||||
{% include 'roles/web-svc-logout/templates/logout-proxy.conf.j2' %}
|
||||
{% endif %}
|
||||
|
||||
location / {
|
||||
try_files /dev/null @api;
|
||||
}
|
||||
|
@@ -26,3 +26,7 @@ docker:
|
||||
services:
|
||||
database:
|
||||
enabled: true
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- pgadmin.{{ primary_domain }}
|
||||
|
@@ -11,3 +11,7 @@ features:
|
||||
ldap: true
|
||||
oauth2: true
|
||||
universal_logout: true
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- phpldapadmin.{{ primary_domain }}
|
||||
|
@@ -22,6 +22,8 @@ domains:
|
||||
aliases:
|
||||
- "mysql.{{ primary_domain }}"
|
||||
- "mariadb.{{ primary_domain }}"
|
||||
canonical:
|
||||
- phpmyadmin.{{ primary_domain }}
|
||||
docker:
|
||||
services:
|
||||
database:
|
||||
|
@@ -30,7 +30,6 @@
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
data:
|
||||
name: {{ pixelfed_volume }}
|
||||
|
||||
|
@@ -18,11 +18,12 @@ features:
|
||||
oauth2: false # Enable the OAuth2-Proy
|
||||
javascript: false # Enables the custom JS in the javascript.js.j2 file
|
||||
universal_logout: true
|
||||
csp:
|
||||
whitelist: {} # URL's which should be whitelisted
|
||||
flags: {} # Flags which should be set
|
||||
csp:
|
||||
whitelist: {} # URL's which should be whitelisted
|
||||
flags: {} # Flags which should be set
|
||||
domains:
|
||||
canonical: {} # Urls under which the domain should be directly accessible
|
||||
canonical:
|
||||
- "pretix.{{ primary_domain }}"
|
||||
aliases: [] # Alias redirections to the first element of the canonical domains
|
||||
rbac:
|
||||
roles: {}
|
||||
|
@@ -18,7 +18,6 @@
|
||||
# Compose Configuration
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
redis:
|
||||
data:
|
||||
name: "{{ snipe_it_volume }}"
|
||||
|
||||
|
@@ -13,3 +13,7 @@ features:
|
||||
# users:
|
||||
# administrator:
|
||||
# username: "{{ users.administrator.username }}"
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- syncope.{{ primary_domain }}
|
||||
|
@@ -1,3 +1,7 @@
|
||||
# xmpp is more a service then a app with ui interface. @todo Rename it
|
||||
features:
|
||||
universal_logout: false # Reactivated as soon as xmpp is fully implemented
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- xmpp.{{ primary_domain }}
|
||||
|
@@ -1,2 +1,6 @@
|
||||
source_directory: "{{ playbook_dir }}/assets"
|
||||
url: "{{ web_protocol }}://<< defaults_applications['web-svc-file']domains.canonical[0] >>/assets"
|
||||
url: "{{ web_protocol }}://<< defaults_applications['web-svc-file']domains.canonical[0] >>/assets"
|
||||
|
||||
domains:
|
||||
canonical:
|
||||
- asset.{{ primary_domain }}
|
||||
|
@@ -2,7 +2,7 @@ import unittest
|
||||
import yaml
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
class TestDomainUniqueness(unittest.TestCase):
|
||||
def test_no_duplicate_domains(self):
|
||||
@@ -22,7 +22,8 @@ class TestDomainUniqueness(unittest.TestCase):
|
||||
cfg = yaml.safe_load(yaml_file.read_text(encoding='utf-8')) or {}
|
||||
apps = cfg.get('defaults_applications', {})
|
||||
|
||||
all_domains = []
|
||||
domain_to_apps = defaultdict(set)
|
||||
|
||||
for app_name, app_cfg in apps.items():
|
||||
domains_cfg = app_cfg.get('domains', {})
|
||||
|
||||
@@ -32,7 +33,10 @@ class TestDomainUniqueness(unittest.TestCase):
|
||||
values = list(canonical.values())
|
||||
else:
|
||||
values = canonical or []
|
||||
all_domains.extend(values)
|
||||
|
||||
for d in values:
|
||||
if isinstance(d, str) and d.strip():
|
||||
domain_to_apps[d].add(app_name)
|
||||
|
||||
# aliases entries may be a list or a mapping
|
||||
aliases = domains_cfg.get('aliases', [])
|
||||
@@ -40,16 +44,16 @@ class TestDomainUniqueness(unittest.TestCase):
|
||||
values = list(aliases.values())
|
||||
else:
|
||||
values = aliases or []
|
||||
all_domains.extend(values)
|
||||
|
||||
# Filter out any empty or non-string entries
|
||||
domain_list = [d for d in all_domains if isinstance(d, str) and d.strip()]
|
||||
counts = Counter(domain_list)
|
||||
for d in values:
|
||||
if isinstance(d, str) and d.strip():
|
||||
domain_to_apps[d].add(app_name)
|
||||
|
||||
# Find duplicates
|
||||
duplicates = [domain for domain, count in counts.items() if count > 1]
|
||||
# Find duplicates: domains that appear in more than one app
|
||||
duplicates = {domain: list(apps) for domain, apps in domain_to_apps.items() if len(apps) > 1}
|
||||
if duplicates:
|
||||
self.fail(f"Duplicate domain entries found: {duplicates}\n (May 'make build' solves this issue.)")
|
||||
details = "\n".join(f"Domain '{domain}' is used in applications: {apps}" for domain, apps in duplicates.items())
|
||||
self.fail(f"Duplicate domain entries found:\n{details}\n(Maybe 'make build' solves this issue.)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
36
tests/integration/test_domains_canonical.py
Normal file
36
tests/integration/test_domains_canonical.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import unittest
|
||||
import yaml
|
||||
import glob
|
||||
import os
|
||||
|
||||
class TestWebRolesDomains(unittest.TestCase):
|
||||
def test_canonical_domains_present_and_not_empty(self):
|
||||
"""
|
||||
Check all roles/web-*/config/main.yml files:
|
||||
- must have domains.canonical defined
|
||||
- domains.canonical must not be empty dict, empty list, or empty string
|
||||
"""
|
||||
role_config_paths = glob.glob("roles/web-*/config/main.yml")
|
||||
self.assertTrue(role_config_paths, "No roles/web-*/config/main.yml files found.")
|
||||
|
||||
for path in role_config_paths:
|
||||
with self.subTest(role_config=path):
|
||||
with open(path, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
self.assertIsInstance(data, dict, f"YAML root is not a dict in {path}")
|
||||
|
||||
domains = data.get("domains")
|
||||
self.assertIsNotNone(domains, f"'domains' section missing in {path}")
|
||||
self.assertIsInstance(domains, dict, f"'domains' must be a dict in {path}")
|
||||
|
||||
canonical = domains.get("canonical")
|
||||
self.assertIsNotNone(canonical, f"'domains.canonical' missing in {path}")
|
||||
|
||||
# Check for emptiness
|
||||
empty_values = [{}, [], ""]
|
||||
self.assertNotIn(canonical, empty_values,
|
||||
f"'domains.canonical' in {path} must not be empty dict, list, or empty string")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Reference in New Issue
Block a user