Index: main/waeup.kofa/trunk/src/waeup/kofa/tests/test_kpasswd.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/tests/test_kpasswd.py	(revision 17820)
+++ main/waeup.kofa/trunk/src/waeup/kofa/tests/test_kpasswd.py	(revision 17820)
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+## $Id$d$: test_smtp.py 16428 2021-03-23 13:15:55Z henrik $
+##
+## Copyright (C) 2024 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+# Tests for kpasswd hash generator
+from __future__ import unicode_literals
+import mock
+import os
+import sys
+import tempfile
+import unittest
+import waeup.kofa.kpasswd
+
+try:
+    from StringIO import StringIO  # py2
+except ImportError:
+    from io import StringIO  # py3
+
+
+SITE_ZCML_TEMPL = """
+<configure xmlns="http://namespaces.zope.org/zope"
+           xmlns:browser="http://namespaces.zope.org/browser"
+           xmlns:kofa="http://namespaces.waeup.org/kofa"
+           i18n_domain="waeup.kofa">
+
+  <include package="waeup.kofa" />
+  <include package="waeup.kofa" file="mail.zcml" />
+  <!-- install job container
+
+       install a job container for asynchronous jobs. Pick one of the
+       two possibilites: 'single' for installation in the one big ZODB
+       or 'multidb' for setup with a dedicated ZODB for job handling.
+  -->
+  <!-- include package="waeup.kofa" file="async_single.zcml" / -->
+  <include package="waeup.kofa" file="async_multidb.zcml" />
+
+  <!-- install dispatcher and other needed async components
+
+       install other needed components for asynchronous jobs. Pick one
+       of the two possibilities: 'basic_dispatcher' for single ZODB
+       setup, 'multidb_dispatcher' for setup with a dedicated ZODB for
+       job handling. Make sure the setting matches the setting picked
+       above. -->
+  <!-- include package="zc.async" file="basic_dispatcher_policy.zcml" / -->
+  <include package="zc.async" file="multidb_dispatcher_policy.zcml" />
+
+  <!-- Where should the datacenter reside by default? -->
+  <kofa:datacenter
+      path="/home/kofa/waeup.kofa/trunk/var/datacenter" />
+
+    <configure i18n_domain="waeup.kofa">
+
+
+      <unauthenticatedPrincipal id="zope.anybody"
+                                title="Unauthenticated User" />
+      <unauthenticatedGroup id="zope.Anybody"
+                            title="Unauthenticated Users" />
+      <authenticatedGroup id="zope.Authenticated"
+                        title="Authenticated Users" />
+      <everybodyGroup id="zope.Everybody"
+                      title="All Users" />
+      <principal id="zope.manager"
+                 title="Manager"
+                 login="grok"
+                 password_manager="SHA1"
+                 password="321b9cd3739bd029629243732eb682db005a7541f7509622"
+                 />
+
+      <!-- XMLRPC user for demo and testing
+           Plaintext password is changeme
+           Don't use it in real deployment -->
+      <principal id="zope.xmlrpcuser1"
+                 title="XMLRPC User 1"
+                 login="xmlrpcuser1"
+                 password="{SSHA}WQ0Mr9Sf7Sc_wxyNaMzjxib3nFFN0Jf0"
+                 password_manager="SSHA"
+                 />
+
+      <grant role="waeup.xmlrpcusers1"
+             principal="zope.xmlrpcuser1" />
+
+
+      <!-- Replace the following directive if you do not want
+           public access -->
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Anybody" />
+      <grant permission="zope.View"
+             principal="zope.Authenticated" />
+      <grant permission="zope.app.dublincore.view"
+             principal="zope.Authenticated" />
+      <grant permission="waeup.Public"
+             principal="zope.Everybody" />
+      <grant permission="waeup.Anonymous"
+             principal="zope.Anybody" />
+      <grant permission="waeup.Authenticated"
+             principal="zope.Authenticated" />
+
+      <role id="zope.Manager" title="Site Manager" />
+      <grantAll role="zope.Manager" />
+      <grant role="zope.Manager"
+             principal="zope.manager" />
+   </configure>
+
+   <!-- session -->
+   <browser:beakerSession
+        key="waeup.kofa.session.id"
+        secret="KofaRocks"
+        timeout="3600"
+        type="cookie"
+        validate_key="thisMightBeChanged"
+        />
+
+</configure>
+"""
+
+
+class KPasswordTests(unittest.TestCase):
+
+    def setUp(self):
+        self.workdir = tempfile.mkdtemp()
+        self.site_zcml = os.path.join(self.workdir, "site.zcml")
+        open(self.site_zcml, "w").write(SITE_ZCML_TEMPL)
+        pass
+
+    @mock.patch("sys.stderr", new_callable=StringIO)
+    def test_parse_args_req_at_least_1_arg(self, mock_err):
+        # we require at least one argument (the password)
+        with self.assertRaises(SystemExit) as cm:
+            waeup.kofa.kpasswd.parse_args(["kpasswd"])
+        self.assertEqual(cm.exception.code, 2)
+        self.assertTrue("error: too few arguments" in mock_err.getvalue())
+
+    @mock.patch("sys.stdout", new_callable=StringIO)
+    def test_main_callable(self, mock_out):
+        result = waeup.kofa.kpasswd.main(["kpasswd", "foo"])
+        output = mock_out.getvalue()
+        self.assertEqual(result, 0)
+        self.assertTrue(output.startswith("{SSHA}"))
+
+    @mock.patch("sys.stderr", new_callable=StringIO)
+    def test_main_considers_argv(self, mock_err):
+        with mock.patch.object(
+            sys,
+            "argv",
+            [
+                "kpasswd-mark1",
+            ],
+        ):
+            waeup.kofa.kpasswd.main()
+        self.assertTrue(
+            "kpasswd-mark1: error: too few arguments" in mock_err.getvalue()
+        )
+
+    @mock.patch("sys.stdout", new_callable=StringIO)
+    def test_application_hash_password(self, mock_out):
+        # we can show an hashed password (SSHA)
+        options = waeup.kofa.kpasswd.parse_args(["kpasswd", "mypassword"])
+        app = waeup.kofa.kpasswd.Application(options)
+        app.run()
+        output = mock_out.getvalue()
+        self.assertTrue(output.startswith("{SSHA}"))
+
+    @mock.patch("sys.stdout", new_callable=StringIO)
+    def test_application_update_site_zcml(self, mock_out):
+        # we can update a given site.zcml file
+        options = waeup.kofa.kpasswd.parse_args(
+            ["kpasswd", "-c", self.site_zcml, "mypassword"]
+        )
+        app = waeup.kofa.kpasswd.Application(options)
+        old_content = open(self.site_zcml).read()
+        app.run()
+        output = mock_out.getvalue()
+        # we get a message, that the file was updated
+        self.assertTrue("Updated: %s" % self.site_zcml in output)
+        new_content = open(self.site_zcml).read()
+        # password hash has changed
+        self.assertNotEqual(old_content, new_content)
+        self.assertTrue("321b9cd3739bd029629243732eb682" not in new_content)
+
+    @mock.patch("sys.stdout", new_callable=StringIO)
+    def test_application_change_non_default_users(self, mock_out):
+        # we can change also users not being `grok`
+        options = waeup.kofa.kpasswd.parse_args(
+            [
+                "kpasswd",
+                "-c",
+                self.site_zcml,
+                "--id",
+                "zope.xmlrpcuser1",
+                "-l",
+                "xmlrpcuser1",
+                "mypassword",
+            ]
+        )
+        app = waeup.kofa.kpasswd.Application(options)
+        old_content = open(self.site_zcml).read()
+        app.run()
+        output = mock_out.getvalue()
+        # we get a message, that the file was updated
+        self.assertTrue("Updated: %s" % self.site_zcml in output)
+        new_content = open(self.site_zcml).read()
+        # password hash has changed
+        self.assertNotEqual(old_content, new_content)
+        # but this time not the one of the zope.manager...
+        self.assertTrue("321b9cd3739bd029629243732eb682" in new_content)
+        self.assertTrue("{SSHA}WQ0Mr9Sf7Sc_wxyNaMzjxib3nFFN0Jf0" not in new_content)
