generated from coulomb/repo-seed
Deliverables: - src/local_identity/gecos.py: /etc/passwd GECOS parsing, current_username() - src/local_identity/user.py: UserRecord dataclass, ProductionIdentity, make_test_user() - Pure test-user derivation: <user>N / +testN email alias / source_user tracking - src/local_identity/store.py: file store CRUD backed by LOCAL_IDENTITY_HOME - ~/.local-identity/ mode 700, user files mode 600 - All path lookups dynamic (env-var override enables clean test isolation) - src/local_identity/cli.py: init/list/show commands; email from flag > config > prompt - pyproject.toml + uv.lock: pyyaml dep, local-identity script entry point Tests (41 passing): - test_gecos.py: 9 tests — simple/comma/empty/non-ASCII/whitespace GECOS, fallback - test_user.py: 14 tests — test-user derivation, YAML roundtrip, non-ASCII, idempotency - test_store.py: 18 tests — dir creation, permissions (700/600), CRUD, list, config, idempotency (reinit with --force produces identical users) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
4.4 KiB
Python
146 lines
4.4 KiB
Python
"""Tests for the file store (read/write users, permissions, config)."""
|
|
|
|
import stat
|
|
|
|
import pytest
|
|
|
|
from local_identity.store import (
|
|
init_dirs,
|
|
store_exists,
|
|
write_user,
|
|
read_user,
|
|
list_users,
|
|
read_config,
|
|
write_config,
|
|
_users_dir,
|
|
_store_dir,
|
|
)
|
|
from local_identity.user import UserRecord, make_test_user
|
|
|
|
ALICE = UserRecord(username="alice", fullname="Alice Smith", email="alice@example.com")
|
|
BOB = UserRecord(username="bob", fullname="Bob Jones", email="bob@example.com")
|
|
|
|
|
|
class TestDirInit:
|
|
def test_creates_store_dir(self, tmp_store):
|
|
assert not tmp_store.exists()
|
|
init_dirs()
|
|
assert tmp_store.exists()
|
|
|
|
def test_store_dir_mode_700(self, tmp_store):
|
|
init_dirs()
|
|
mode = stat.S_IMODE(tmp_store.stat().st_mode)
|
|
assert mode == 0o700, f"expected 0o700, got 0o{mode:03o}"
|
|
|
|
def test_users_dir_mode_700(self, tmp_store):
|
|
init_dirs()
|
|
users = tmp_store / "users"
|
|
mode = stat.S_IMODE(users.stat().st_mode)
|
|
assert mode == 0o700, f"expected 0o700, got 0o{mode:03o}"
|
|
|
|
def test_idempotent(self, tmp_store):
|
|
init_dirs()
|
|
init_dirs() # should not raise
|
|
assert tmp_store.exists()
|
|
|
|
def test_store_exists_false_before_init(self, tmp_store):
|
|
assert not store_exists()
|
|
|
|
def test_store_exists_true_after_init(self, tmp_store):
|
|
init_dirs()
|
|
assert store_exists()
|
|
|
|
|
|
class TestWriteReadUser:
|
|
def test_roundtrip(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
loaded = read_user("alice")
|
|
assert loaded.username == "alice"
|
|
assert loaded.fullname == "Alice Smith"
|
|
assert loaded.email == "alice@example.com"
|
|
|
|
def test_file_mode_600(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
path = _users_dir() / "alice.yaml"
|
|
mode = stat.S_IMODE(path.stat().st_mode)
|
|
assert mode == 0o600, f"expected 0o600, got 0o{mode:03o}"
|
|
|
|
def test_overwrite_same_user(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
updated = UserRecord(username="alice", fullname="Alice Updated", email="alice@example.com")
|
|
write_user(updated)
|
|
assert read_user("alice").fullname == "Alice Updated"
|
|
|
|
def test_read_nonexistent_raises(self, tmp_store):
|
|
init_dirs()
|
|
with pytest.raises(FileNotFoundError, match="nobody"):
|
|
read_user("nobody")
|
|
|
|
|
|
class TestListUsers:
|
|
def test_empty_store(self, tmp_store):
|
|
init_dirs()
|
|
assert list_users() == []
|
|
|
|
def test_lists_all_users(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
write_user(BOB)
|
|
usernames = {u.username for u in list_users()}
|
|
assert usernames == {"alice", "bob"}
|
|
|
|
def test_includes_test_users(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
write_user(make_test_user(ALICE, 1))
|
|
write_user(make_test_user(ALICE, 2))
|
|
usernames = {u.username for u in list_users()}
|
|
assert usernames == {"alice", "alice1", "alice2"}
|
|
|
|
def test_sorted_by_filename(self, tmp_store):
|
|
init_dirs()
|
|
write_user(BOB)
|
|
write_user(ALICE)
|
|
names = [u.username for u in list_users()]
|
|
assert names == sorted(names)
|
|
|
|
|
|
class TestConfig:
|
|
def test_read_config_empty(self, tmp_store):
|
|
init_dirs()
|
|
assert read_config() == {}
|
|
|
|
def test_write_read_config(self, tmp_store):
|
|
init_dirs()
|
|
write_config({"email": "tegwick@example.com"})
|
|
assert read_config()["email"] == "tegwick@example.com"
|
|
|
|
def test_config_mode_600(self, tmp_store):
|
|
init_dirs()
|
|
write_config({"email": "x@y.com"})
|
|
from local_identity.store import _config_file
|
|
mode = stat.S_IMODE(_config_file().stat().st_mode)
|
|
assert mode == 0o600
|
|
|
|
|
|
class TestIdempotency:
|
|
def test_reinit_produces_same_users(self, tmp_store):
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
write_user(make_test_user(ALICE, 1))
|
|
write_user(make_test_user(ALICE, 2))
|
|
|
|
# Simulate --force re-init with same inputs
|
|
init_dirs()
|
|
write_user(ALICE)
|
|
write_user(make_test_user(ALICE, 1))
|
|
write_user(make_test_user(ALICE, 2))
|
|
|
|
users = {u.username: u for u in list_users()}
|
|
assert set(users.keys()) == {"alice", "alice1", "alice2"}
|
|
assert users["alice1"].source_user == "alice"
|
|
assert users["alice1"].email == "alice+test1@example.com"
|