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