Files
shopdb-flask/tests/test_plugins/test_employees_resolve.py
cproudlock 221bbb226e
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
employees: an endpoint that says WHY a name did not resolve
The shopfloor board lost every photo and started showing lowercase SSOs where
names belong, and nothing in the system could say why. Both resolvers returned
None on any problem and three separate bare excepts threw the cause away, so an
unreachable HR host, a rotated credential, a renamed column and a genuinely
unknown SSO all produced identical output and no log line. That is not a bug in
the resolution so much as a hole where the diagnosis should be.

GET /api/employees/resolve/<sso> returns the same answer the board gets, plus
which source produced it (directory, hrdirectory, useaccount), which mode the
directory is in, and the exception text when a source failed. It is the
difference between "the board is broken" and "the HR host refused the
connection".

The two bare excepts in the shared resolvers now log rather than pass. The
external-directory branch is the one that fails on a live site; it was the one
saying nothing.

No behaviour change to the board itself - it still falls back exactly as
before. What changed is that the fallback is now visible.
2026-08-11 16:10:20 -04:00

69 lines
2.7 KiB
Python

"""Resolving an SSO says HOW it resolved, or why it did not.
Built after the shopfloor board lost every photo and showed lowercase SSOs
instead of names, and nothing in the system could say why. Both resolvers
returned None on any problem, so an unreachable HR host, a rotated credential
and a genuinely unknown SSO were indistinguishable - and none of them were
logged. This endpoint exists to tell those apart.
"""
URL = '/api/employees/resolve/'
def _selfhosted(db):
from shopdb.core.models import Setting
row = Setting.query.filter_by(key='employee_directory_mode').first()
if row:
row.value = 'selfhosted'
else:
db.session.add(Setting(key='employee_directory_mode', value='selfhosted'))
db.session.commit()
def test_a_known_employee_resolves_and_names_its_source(client, db, auth_headers):
from plugins.employees.models import DirectoryEmployee
_selfhosted(db)
db.session.add(DirectoryEmployee(sso=502123, firstname='Ada',
lastname='Lovelace'))
db.session.commit()
data = client.get(URL + '502123', headers=auth_headers).get_json()['data']
assert data['name'] == 'Ada Lovelace'
assert data['source'] == 'directory'
assert data['mode'] == 'selfhosted'
assert data['error'] is None
def test_an_unknown_sso_says_so_rather_than_returning_nothing(client, db,
auth_headers):
"""The board shows a lowercase SSO for this case AND for a broken
directory. The whole point is that they now read differently."""
_selfhosted(db)
data = client.get(URL + '999999', headers=auth_headers).get_json()['data']
assert data['name'] is None
assert data['error']
def test_a_user_account_answers_when_the_directory_does_not(client, db,
auth_headers):
"""Someone from another site has no directory row but does have a login.
That fallback exists; without this endpoint nothing showed it had fired."""
from shopdb.core.models import User
_selfhosted(db)
db.session.add(User(username='701234', firstname='Grace', lastname='Hopper',
email='g@example.com', passwordhash='x'))
db.session.commit()
data = client.get(URL + '701234', headers=auth_headers).get_json()['data']
assert data['name'] == 'Grace Hopper'
assert data['source'] == 'useraccount'
def test_a_non_numeric_sso_is_rejected_clearly(client, db, auth_headers):
data = client.get(URL + 'abc', headers=auth_headers).get_json()['data']
assert data['error'] == 'SSO must be digits'
def test_the_endpoint_needs_a_login(client, db):
assert client.get(URL + '502123').status_code == 401