generated from coulomb/repo-seed
export.py:
- split_fullname(): last-token strategy (Bernd Worsch → firstName/lastName)
- _deterministic_id(): uuid5(DNS, "local-identity.{realm}.{username}") for stable,
re-import-idempotent Keycloak IDs
- user_to_keycloak(): full Keycloak Admin REST API user representation;
production_identity mapping applied to username + realm; isolation attributes
(local_identity_environment, local_identity_generated) always present;
validate_keycloak_user() called on every conversion to catch schema drift
- bulk_export_body(): partial import body (ifResourceExists/realm/users)
cli.py: add `export` subcommand
- export <username> single user, prints Keycloak JSON
- export (no args) bulk; primary users only; stderr note on skipped test users
- export --include-test bulk; all users including generated
- --realm / --if-resource-exists flags
docs/LocalIdentity.md: add two new sections
- Keycloak import procedure: export → partialImport API → password reset → retire
- Isolation guarantee: attribute schema, Keycloak Condition authenticator config,
production_identity mapping walkthrough
tests/test_export.py: 34 new tests (88 total, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
7.0 KiB
Python
199 lines
7.0 KiB
Python
"""Tests for the Keycloak export module."""
|
|
|
|
import json
|
|
import uuid
|
|
|
|
import pytest
|
|
|
|
from local_identity.export import (
|
|
_deterministic_id,
|
|
bulk_export_body,
|
|
split_fullname,
|
|
user_to_keycloak,
|
|
validate_keycloak_user,
|
|
)
|
|
from local_identity.user import ProductionIdentity, UserRecord, make_test_user
|
|
|
|
PRIMARY = UserRecord(
|
|
username="tegwick",
|
|
fullname="Bernd Worsch",
|
|
email="bernd.worsch@gmail.com",
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# split_fullname #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class TestSplitFullname:
|
|
def test_two_words(self):
|
|
assert split_fullname("Bernd Worsch") == ("Bernd", "Worsch")
|
|
|
|
def test_three_words(self):
|
|
assert split_fullname("Jean Claude Damme") == ("Jean Claude", "Damme")
|
|
|
|
def test_single_word(self):
|
|
assert split_fullname("Madonna") == ("Madonna", "")
|
|
|
|
def test_extra_whitespace_stripped(self):
|
|
assert split_fullname(" Bernd Worsch ") == ("Bernd", "Worsch")
|
|
|
|
def test_non_ascii(self):
|
|
first, last = split_fullname("Ärger Müller")
|
|
assert first == "Ärger"
|
|
assert last == "Müller"
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# _deterministic_id #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class TestDeterministicId:
|
|
def test_stable_across_calls(self):
|
|
assert _deterministic_id("net-kingdom", "tegwick") == _deterministic_id("net-kingdom", "tegwick")
|
|
|
|
def test_different_realm_gives_different_id(self):
|
|
assert _deterministic_id("realm-a", "u") != _deterministic_id("realm-b", "u")
|
|
|
|
def test_different_username_gives_different_id(self):
|
|
assert _deterministic_id("r", "alice") != _deterministic_id("r", "bob")
|
|
|
|
def test_is_valid_uuid(self):
|
|
uid = _deterministic_id("net-kingdom", "tegwick")
|
|
uuid.UUID(uid) # must not raise
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# user_to_keycloak #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class TestUserToKeycloak:
|
|
def test_username(self):
|
|
assert user_to_keycloak(PRIMARY)["username"] == "tegwick"
|
|
|
|
def test_name_split(self):
|
|
kc = user_to_keycloak(PRIMARY)
|
|
assert kc["firstName"] == "Bernd"
|
|
assert kc["lastName"] == "Worsch"
|
|
|
|
def test_email(self):
|
|
assert user_to_keycloak(PRIMARY)["email"] == "bernd.worsch@gmail.com"
|
|
|
|
def test_enabled_and_email_verified(self):
|
|
kc = user_to_keycloak(PRIMARY)
|
|
assert kc["enabled"] is True
|
|
assert kc["emailVerified"] is False
|
|
|
|
def test_environment_attribute(self):
|
|
kc = user_to_keycloak(PRIMARY)
|
|
assert kc["attributes"]["local_identity_environment"] == ["local"]
|
|
|
|
def test_primary_user_has_no_generated_attribute(self):
|
|
kc = user_to_keycloak(PRIMARY)
|
|
assert "local_identity_generated" not in kc["attributes"]
|
|
|
|
def test_test_user_has_generated_attribute(self):
|
|
t = make_test_user(PRIMARY, 1)
|
|
kc = user_to_keycloak(t)
|
|
assert kc["attributes"]["local_identity_generated"] == ["true"]
|
|
|
|
def test_structural_fields_present(self):
|
|
kc = user_to_keycloak(PRIMARY)
|
|
for field in ("credentials", "requiredActions", "groups", "realmRoles", "clientRoles"):
|
|
assert field in kc, f"missing field '{field}'"
|
|
|
|
def test_id_is_deterministic(self):
|
|
assert user_to_keycloak(PRIMARY)["id"] == user_to_keycloak(PRIMARY)["id"]
|
|
|
|
def test_id_is_valid_uuid(self):
|
|
uuid.UUID(user_to_keycloak(PRIMARY)["id"])
|
|
|
|
def test_json_serializable(self):
|
|
json.dumps(user_to_keycloak(PRIMARY)) # must not raise
|
|
|
|
def test_production_identity_overrides_username(self):
|
|
u = UserRecord(
|
|
username="tegwick",
|
|
fullname="Bernd Worsch",
|
|
email="bernd.worsch@gmail.com",
|
|
production_identity=ProductionIdentity(username="bworsch", realm="prod"),
|
|
)
|
|
assert user_to_keycloak(u)["username"] == "bworsch"
|
|
|
|
def test_production_identity_realm_used_in_id(self):
|
|
u = UserRecord(
|
|
username="tegwick",
|
|
fullname="Bernd Worsch",
|
|
email="bernd.worsch@gmail.com",
|
|
production_identity=ProductionIdentity(username="bworsch", realm="prod"),
|
|
)
|
|
expected_id = _deterministic_id("prod", "bworsch")
|
|
assert user_to_keycloak(u)["id"] == expected_id
|
|
|
|
def test_default_realm_used_when_no_production_identity(self):
|
|
expected_id = _deterministic_id("my-realm", "tegwick")
|
|
assert user_to_keycloak(PRIMARY, realm="my-realm")["id"] == expected_id
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# validate_keycloak_user #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class TestValidateKeycloakUser:
|
|
def test_valid_passes(self):
|
|
validate_keycloak_user(user_to_keycloak(PRIMARY)) # must not raise
|
|
|
|
def test_missing_field_raises(self):
|
|
d = user_to_keycloak(PRIMARY)
|
|
del d["username"]
|
|
with pytest.raises(ValueError, match="username"):
|
|
validate_keycloak_user(d)
|
|
|
|
def test_wrong_type_raises(self):
|
|
d = user_to_keycloak(PRIMARY)
|
|
d["enabled"] = "yes"
|
|
with pytest.raises(ValueError, match="enabled"):
|
|
validate_keycloak_user(d)
|
|
|
|
def test_multiple_errors_all_reported(self):
|
|
d = user_to_keycloak(PRIMARY)
|
|
del d["username"]
|
|
del d["email"]
|
|
with pytest.raises(ValueError) as exc_info:
|
|
validate_keycloak_user(d)
|
|
msg = str(exc_info.value)
|
|
assert "username" in msg
|
|
assert "email" in msg
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# bulk_export_body #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
class TestBulkExportBody:
|
|
def test_required_keys_present(self):
|
|
body = bulk_export_body([PRIMARY])
|
|
assert "ifResourceExists" in body
|
|
assert "realm" in body
|
|
assert "users" in body
|
|
|
|
def test_default_if_resource_exists(self):
|
|
assert bulk_export_body([PRIMARY])["ifResourceExists"] == "SKIP"
|
|
|
|
def test_custom_realm(self):
|
|
assert bulk_export_body([PRIMARY], realm="my-realm")["realm"] == "my-realm"
|
|
|
|
def test_user_count(self):
|
|
users = [PRIMARY, make_test_user(PRIMARY, 1), make_test_user(PRIMARY, 2)]
|
|
assert len(bulk_export_body(users)["users"]) == 3
|
|
|
|
def test_empty_user_list(self):
|
|
assert bulk_export_body([])["users"] == []
|
|
|
|
def test_overwrite_strategy(self):
|
|
body = bulk_export_body([PRIMARY], if_resource_exists="OVERWRITE")
|
|
assert body["ifResourceExists"] == "OVERWRITE"
|
|
|
|
def test_json_serializable(self):
|
|
json.dumps(bulk_export_body([PRIMARY, make_test_user(PRIMARY, 1)]))
|