From 65663de7a7d17245ef262079749f1fb3d8608f00 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 6 Jan 2026 16:11:04 +0100 Subject: [PATCH] ci: add unit test workflow and package setup for ldapsm - Add GitHub Actions workflow to run make test - Introduce pyproject.toml with python-ldap dependency and CLI entrypoint - Add Makefile with install/test/clean targets - Move tests to src-based package layout and fix imports - Remove legacy requirements.yml https://chatgpt.com/share/695d2615-d664-800f-b821-5705c631bfe8 --- .github/workflows/tests.yml | 41 ++++++++++ Makefile | 23 ++++++ pyproject.toml | 28 +++++++ requirements.yml | 2 - src/ldapsm.egg-info/PKG-INFO | 67 ++++++++++++++++ src/ldapsm.egg-info/SOURCES.txt | 10 +++ src/ldapsm.egg-info/dependency_links.txt | 1 + src/ldapsm.egg-info/entry_points.txt | 2 + src/ldapsm.egg-info/requires.txt | 1 + src/ldapsm.egg-info/top_level.txt | 1 + src/ldapsm/__init__.py | 5 ++ main.py => src/ldapsm/__main__.py | 0 test_manage_schema.py | 79 ------------------- tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/test_manage_schema.py | 98 ++++++++++++++++++++++++ 16 files changed, 277 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 Makefile create mode 100644 pyproject.toml delete mode 100644 requirements.yml create mode 100644 src/ldapsm.egg-info/PKG-INFO create mode 100644 src/ldapsm.egg-info/SOURCES.txt create mode 100644 src/ldapsm.egg-info/dependency_links.txt create mode 100644 src/ldapsm.egg-info/entry_points.txt create mode 100644 src/ldapsm.egg-info/requires.txt create mode 100644 src/ldapsm.egg-info/top_level.txt create mode 100644 src/ldapsm/__init__.py rename main.py => src/ldapsm/__main__.py (100%) delete mode 100644 test_manage_schema.py create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/test_manage_schema.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..7a5f62b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: CI - tests + +on: + push: + branches: ["**"] + tags: ["**"] + pull_request: + workflow_dispatch: + +jobs: + test: + name: Unit tests (make test) + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install system dependencies for python-ldap + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + gcc \ + python3-dev \ + libldap2-dev \ + libsasl2-dev \ + libssl-dev + + - name: Install project + run: | + python -m pip install --upgrade pip + python -m pip install -e . + + - name: Run tests + run: | + make test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8b24043 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: help install test lint clean + +PYTHON ?= python3 +PROJECT_NAME := ldapsm +SRC_DIR := src +TEST_DIR := tests + +help: + @echo "Available targets:" + @echo " install Install project in editable mode" + @echo " test Run unit tests" + @echo " clean Remove Python cache files" + +install: + $(PYTHON) -m pip install --upgrade pip + $(PYTHON) -m pip install -e . + +test: + $(PYTHON) -m unittest discover -s $(TEST_DIR) -p "test_*.py" + +clean: + find . -type d -name "__pycache__" -exec rm -rf {} + + find . -type f -name "*.pyc" -delete diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0bb2953 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "ldapsm" +version = "0.1.0" +description = "CLI tool to manage OpenLDAP schema snippets under cn=config" +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } + +authors = [ + { name = "Kevin Veen-Birkenbach" } +] + +dependencies = [ + "python-ldap>=3.4", +] + +[project.scripts] +ldapsm = "ldapsm.__main__:main" + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/requirements.yml b/requirements.yml deleted file mode 100644 index 68fc87a..0000000 --- a/requirements.yml +++ /dev/null @@ -1,2 +0,0 @@ -pacman: - - python-ldap \ No newline at end of file diff --git a/src/ldapsm.egg-info/PKG-INFO b/src/ldapsm.egg-info/PKG-INFO new file mode 100644 index 0000000..dbd7d8a --- /dev/null +++ b/src/ldapsm.egg-info/PKG-INFO @@ -0,0 +1,67 @@ +Metadata-Version: 2.4 +Name: ldapsm +Version: 0.1.0 +Summary: CLI tool to manage OpenLDAP schema snippets under cn=config +Author: Kevin Veen-Birkenbach +License: MIT +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: python-ldap>=3.4 +Dynamic: license-file + +# LDAP Schema Manager 🛠️ +[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-blue?logo=github)](https://github.com/sponsors/kevinveenbirkenbach) [![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://s.veen.world/paypaldonate) + + +A Python-based CLI tool for managing OpenLDAP schema snippets under `cn=config`, allowing you to create or update schema entries—including custom `olcAttributeTypes` and `olcObjectClasses`—via LDAPI. + +## 🚀 Installation + +You can install **ldapsm** easily using [Kevin's package manager](https://github.com/kevinveenbirkenbach/package-manager): + +```bash +pkgmgr install ldapsm +``` + +## 📝 Usage + +After installation, run: + +```bash +ldapsm --help +``` + +to view all available commands and options. + +### Example + +```bash +ldapsm \ + -s ldapi:/// \ + -D "" \ + -W "" \ + -n nextcloud \ + -a "( 1.3.6.1.4.1.99999.1 NAME 'nextcloudQuota' DESC 'Quota for Nextcloud' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )" \ + -c "( 1.3.6.1.4.1.99999.2 NAME 'nextcloudUser' DESC 'Auxiliary class for Nextcloud attributes' AUXILIARY MAY ( nextcloudQuota ) )" +``` + +## 📖 Help + +For detailed usage and options, run: + +```bash +ldapsm --help +``` + +## 🛡️ Contributing + +Contributions, issues, and feature requests are welcome! Feel free to check [issues](https://github.com/kevinveenbirkenbach/ldap-schema-manager/issues). + +## 📜 License + +This project is licensed under the MIT License. + +--- + +**Author:** [Kevin Veen-Birkenbach](https://www.veen.world/) diff --git a/src/ldapsm.egg-info/SOURCES.txt b/src/ldapsm.egg-info/SOURCES.txt new file mode 100644 index 0000000..30f3cde --- /dev/null +++ b/src/ldapsm.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +LICENSE +README.md +pyproject.toml +src/ldapsm/__main__.py +src/ldapsm.egg-info/PKG-INFO +src/ldapsm.egg-info/SOURCES.txt +src/ldapsm.egg-info/dependency_links.txt +src/ldapsm.egg-info/entry_points.txt +src/ldapsm.egg-info/requires.txt +src/ldapsm.egg-info/top_level.txt \ No newline at end of file diff --git a/src/ldapsm.egg-info/dependency_links.txt b/src/ldapsm.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/ldapsm.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/ldapsm.egg-info/entry_points.txt b/src/ldapsm.egg-info/entry_points.txt new file mode 100644 index 0000000..8eff0dd --- /dev/null +++ b/src/ldapsm.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +ldapsm = ldapsm.__main__:main diff --git a/src/ldapsm.egg-info/requires.txt b/src/ldapsm.egg-info/requires.txt new file mode 100644 index 0000000..85341a3 --- /dev/null +++ b/src/ldapsm.egg-info/requires.txt @@ -0,0 +1 @@ +python-ldap>=3.4 diff --git a/src/ldapsm.egg-info/top_level.txt b/src/ldapsm.egg-info/top_level.txt new file mode 100644 index 0000000..dca1325 --- /dev/null +++ b/src/ldapsm.egg-info/top_level.txt @@ -0,0 +1 @@ +ldapsm diff --git a/src/ldapsm/__init__.py b/src/ldapsm/__init__.py new file mode 100644 index 0000000..0ed339a --- /dev/null +++ b/src/ldapsm/__init__.py @@ -0,0 +1,5 @@ +""" +ldapsm package. + +The CLI entry point is implemented in ldapsm.__main__. +""" diff --git a/main.py b/src/ldapsm/__main__.py similarity index 100% rename from main.py rename to src/ldapsm/__main__.py diff --git a/test_manage_schema.py b/test_manage_schema.py deleted file mode 100644 index 81fdaf7..0000000 --- a/test_manage_schema.py +++ /dev/null @@ -1,79 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock -import main as manage_schema -import sys - -class TestManageSchema(unittest.TestCase): - - def test_normalize_removes_extra_whitespace(self): - input_data = b' ( 1.2.3.4 NAME \'foo\' \n DESC \t "bar" ) ' - expected = b'( 1.2.3.4 NAME \'foo\' DESC "bar" )' - self.assertEqual(manage_schema.normalize(input_data), expected) - - @patch('ldap.initialize') - def test_existing_attribute_type_replaced(self, mock_ldap_init): - # Setup mock connection and search result - mock_conn = MagicMock() - mock_ldap_init.return_value = mock_conn - - # Fake result for olcAttributeTypes containing old version - existing_attr = b"( 1.3.6.1.4.1.99999.1 NAME 'nextcloudQuota' DESC 'Old desc' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" - mock_conn.search_s.return_value = [(None, {'olcAttributeTypes': [existing_attr]})] - - # Call modify_s and simulate replace - atdef = "( 1.3.6.1.4.1.99999.1 NAME 'nextcloudQuota' DESC 'Quota for Nextcloud' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )" - dn = "cn={0}nextcloud,cn=schema,cn=config" - - # Manually test logic: normalize and modify decision - norm_existing = [manage_schema.normalize(existing_attr)] - norm_new = manage_schema.normalize(atdef.encode()) - self.assertNotEqual(norm_existing[0], norm_new) - - # Simulate replace - mock_conn.modify_s.reset_mock() - mock_conn.modify_s(dn, [(2, 'olcAttributeTypes', [atdef.encode()])]) - mock_conn.modify_s.assert_called_once_with( - dn, [(2, 'olcAttributeTypes', [atdef.encode()])] - ) - - @patch('ldap.initialize') - def test_add_new_object_class(self, mock_ldap_init): - mock_conn = MagicMock() - mock_ldap_init.return_value = mock_conn - - # No existing object classes - mock_conn.search_s.return_value = [(None, {'olcObjectClasses': []})] - - ocdef = "( 1.3.6.1.4.1.99999.2 NAME 'nextcloudUser' DESC 'Auxiliary class' AUXILIARY MAY ( nextcloudQuota ) )" - encoded = ocdef.encode() - - # Simulate adding - dn = "cn={0}nextcloud,cn=schema,cn=config" - mock_conn.modify_s(dn, [(0, 'olcObjectClasses', [encoded])]) - mock_conn.modify_s.assert_called_once_with( - dn, [(0, 'olcObjectClasses', [encoded])] - ) - - # Test no double add - mock_conn.modify_s.reset_mock() - mock_conn.search_s.return_value = [("cn={0}nextcloud,cn=schema,cn=config", {'olcObjectClasses': [encoded]})] - with patch.object(sys, 'argv', [ - 'manage_schema.py', - '-s', 'ldap://localhost', - '-D', 'cn=admin,dc=example,dc=org', - '-W', 'secret', - '-n', 'nextcloud', - '-c', ocdef - ]): - manage_schema.main() - mock_conn.modify_s.assert_not_called() - - - def test_extract_oid_from_definition(self): - ldif = "( 1.3.6.1.4.1.99999.1 NAME 'example' DESC 'something' )" - oid = manage_schema.extract_oid(ldif) - self.assertEqual(oid, "1.3.6.1.4.1.99999.1") - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_manage_schema.py b/tests/unit/test_manage_schema.py new file mode 100644 index 0000000..0268fc4 --- /dev/null +++ b/tests/unit/test_manage_schema.py @@ -0,0 +1,98 @@ +import sys +import unittest +from unittest.mock import MagicMock, patch + +import ldapsm.__main__ as manage_schema + + +class TestManageSchema(unittest.TestCase): + def test_normalize_removes_extra_whitespace(self): + input_data = b' ( 1.2.3.4 NAME \'foo\' \n DESC \t "bar" ) ' + expected = b'( 1.2.3.4 NAME \'foo\' DESC "bar" )' + self.assertEqual(manage_schema.normalize(input_data), expected) + + def test_extract_oid_from_definition(self): + ldif = "( 1.3.6.1.4.1.99999.1 NAME 'example' DESC 'something' )" + oid = manage_schema.extract_oid(ldif) + self.assertEqual(oid, "1.3.6.1.4.1.99999.1") + + @patch("ldapsm.__main__.ldap.initialize") + def test_add_new_object_class_no_double_add(self, mock_ldap_init): + mock_conn = MagicMock() + mock_ldap_init.return_value = mock_conn + + schema_name = "nextcloud" + ocdef = "( 1.3.6.1.4.1.99999.2 NAME 'nextcloudUser' DESC 'Auxiliary class' AUXILIARY MAY ( nextcloudQuota ) )" + encoded = ocdef.encode() + + mock_conn.search_s.side_effect = [ + [(f"cn={{0}}{schema_name},cn=schema,cn=config", {"dn": [b"dummy"]})], + [(f"cn={{0}}{schema_name},cn=schema,cn=config", {"olcObjectClasses": [encoded]})], + ] + + with patch.object( + sys, + "argv", + [ + "ldapsm", + "-s", + "ldap://localhost", + "-D", + "cn=admin,dc=example,dc=org", + "-W", + "secret", + "-n", + schema_name, + "-c", + ocdef, + ], + ): + manage_schema.main() + + mock_conn.modify_s.assert_not_called() + + @patch("ldapsm.__main__.ldap.initialize") + def test_replace_attribute_type_same_oid(self, mock_ldap_init): + mock_conn = MagicMock() + mock_ldap_init.return_value = mock_conn + + schema_name = "nextcloud" + atdef = "( 1.3.6.1.4.1.99999.1 NAME 'nextcloudQuota' DESC 'Quota for Nextcloud' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )" + encoded = atdef.encode() + + existing_attr_old = b"( 1.3.6.1.4.1.99999.1 NAME 'nextcloudQuota' DESC 'Old desc' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" + + mock_conn.search_s.side_effect = [ + [(f"cn={{0}}{schema_name},cn=schema,cn=config", {"dn": [b"dummy"]})], + [(f"cn={{0}}{schema_name},cn=schema,cn=config", {"olcAttributeTypes": [existing_attr_old]})], + ] + + with patch.object( + sys, + "argv", + [ + "ldapsm", + "-s", + "ldap://localhost", + "-D", + "cn=admin,dc=example,dc=org", + "-W", + "secret", + "-n", + schema_name, + "-a", + atdef, + ], + ): + manage_schema.main() + + mock_conn.modify_s.assert_called_once() + args, _kwargs = mock_conn.modify_s.call_args + self.assertIn("cn={0}nextcloud,cn=schema,cn=config", args[0]) + self.assertEqual(args[1][0][0], manage_schema.ldap.MOD_REPLACE) + self.assertEqual(args[1][0][1], "olcAttributeTypes") + self.assertEqual(args[1][0][2], [encoded]) + + +if __name__ == "__main__": + unittest.main()