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:
41
.github/workflows/tests.yml
vendored
Normal file
41
.github/workflows/tests.yml
vendored
Normal 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
23
Makefile
Normal 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
28
pyproject.toml
Normal 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"]
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pacman:
|
|
||||||
- python-ldap
|
|
||||||
67
src/ldapsm.egg-info/PKG-INFO
Normal file
67
src/ldapsm.egg-info/PKG-INFO
Normal 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 🛠️
|
||||||
|
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](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/)
|
||||||
10
src/ldapsm.egg-info/SOURCES.txt
Normal file
10
src/ldapsm.egg-info/SOURCES.txt
Normal 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
|
||||||
1
src/ldapsm.egg-info/dependency_links.txt
Normal file
1
src/ldapsm.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
2
src/ldapsm.egg-info/entry_points.txt
Normal file
2
src/ldapsm.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
ldapsm = ldapsm.__main__:main
|
||||||
1
src/ldapsm.egg-info/requires.txt
Normal file
1
src/ldapsm.egg-info/requires.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
python-ldap>=3.4
|
||||||
1
src/ldapsm.egg-info/top_level.txt
Normal file
1
src/ldapsm.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ldapsm
|
||||||
5
src/ldapsm/__init__.py
Normal file
5
src/ldapsm/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""
|
||||||
|
ldapsm package.
|
||||||
|
|
||||||
|
The CLI entry point is implemented in ldapsm.__main__.
|
||||||
|
"""
|
||||||
@@ -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
0
tests/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
98
tests/unit/test_manage_schema.py
Normal file
98
tests/unit/test_manage_schema.py
Normal 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()
|
||||||
Reference in New Issue
Block a user