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
This commit is contained in:
2026-01-06 16:11:04 +01:00
parent b9b6c22e89
commit 65663de7a7
16 changed files with 277 additions and 81 deletions

41
.github/workflows/tests.yml vendored Normal file
View File

@@ -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

23
Makefile Normal file
View File

@@ -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

28
pyproject.toml Normal file
View File

@@ -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"]

View File

@@ -1,2 +0,0 @@
pacman:
- python-ldap

View File

@@ -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/)

View File

@@ -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

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,2 @@
[console_scripts]
ldapsm = ldapsm.__main__:main

View File

@@ -0,0 +1 @@
python-ldap>=3.4

View File

@@ -0,0 +1 @@
ldapsm

5
src/ldapsm/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""
ldapsm package.
The CLI entry point is implemented in ldapsm.__main__.
"""

View File

@@ -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()

0
tests/__init__.py Normal file
View File

0
tests/unit/__init__.py Normal file
View File

View File

@@ -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()