import pathlib import sys import unittest sys.path.insert(0, str(pathlib.Path(__file__).parents[1])) from password_setup import PasswordSetupGrants class PasswordSetupGrantTests(unittest.TestCase): def setUp(self): self.now = 100.0 self.calls = [] self.grants = PasswordSetupGrants( public_url="https://kc.example/setup/password", setter=lambda subject, password: self.calls.append((subject, password)), ttl_seconds=60, clock=lambda: self.now, ) def token(self, url): return url.partition("token=")[2] def test_grant_is_single_use(self): token = self.token(self.grants.issue("binky-admin")) self.assertTrue(self.grants.valid(token)) self.grants.consume(token, "a-secure-password") self.assertEqual([("binky-admin", "a-secure-password")], self.calls) self.assertFalse(self.grants.valid(token)) with self.assertRaisesRegex(ValueError, "invalid or expired"): self.grants.consume(token, "a-secure-password") def test_new_grant_revokes_previous_subject_grant(self): first = self.token(self.grants.issue("binky-admin")) second = self.token(self.grants.issue("binky-admin")) self.assertFalse(self.grants.valid(first)) self.assertTrue(self.grants.valid(second)) def test_expired_grant_fails_closed(self): token = self.token(self.grants.issue("binky-admin")) self.now = 161.0 self.assertFalse(self.grants.valid(token)) with self.assertRaisesRegex(ValueError, "invalid or expired"): self.grants.consume(token, "a-secure-password") def test_password_policy_precedes_consumption(self): token = self.token(self.grants.issue("binky-admin")) with self.assertRaisesRegex(ValueError, "12 characters"): self.grants.consume(token, "too-short") self.assertTrue(self.grants.valid(token)) class CompanyReturnTests(unittest.TestCase): def test_return_is_bound_to_grant_and_cannot_be_changed_by_browser(self): mapping = {"tenant:trial:demo-company": "https://vergabe.example/demo-company/"} grants = PasswordSetupGrants(public_url="https://kc.example/setup/password", setter=lambda *args: None, tenant_returns=mapping) url = grants.issue("recipient", "tenant:trial:demo-company") token = url.partition("token=")[2] mapping["tenant:trial:demo-company"] = "https://attacker.example/" self.assertNotIn("recipient", url) self.assertNotIn("return", url) self.assertEqual("https://vergabe.example/demo-company/", grants.consume(token, "test-password-long")) with self.assertRaises(ValueError): grants.consume(token, "test-password-long") def test_unknown_tenant_has_no_return_and_bad_targets_fail(self): for target in ("http://example.test/", "https://example.test/?next=evil", "https://example.test/#fragment", "https://user:password@example.test/"): with self.assertRaises(ValueError): PasswordSetupGrants(public_url="https://kc.example/setup/password", setter=lambda *args: None, tenant_returns={"tenant:trial:demo": target}) grants = PasswordSetupGrants(public_url="https://kc.example/setup/password", setter=lambda *args: None) token = grants.issue("recipient", "tenant:unknown").partition("token=")[2] self.assertEqual("", grants.consume(token, "test-password-long")) def test_expiry_and_failure_never_release_a_return(self): clock = [0] grants = PasswordSetupGrants(public_url="https://kc.example/setup/password", setter=lambda *args: None, clock=lambda: clock[0], ttl_seconds=1, tenant_returns={"tenant:trial:demo": "https://app.example/demo/"}) token = grants.issue("recipient", "tenant:trial:demo").partition("token=")[2] clock[0] = 2 with self.assertRaises(ValueError): grants.consume(token, "test-password-long")