"""The employee learning centre: language resolution, filters, facets. The language rules are the ones with teeth. One decider (the server), a documented fallback chain, and an explicit flag telling the reader when they got a fallback — because silently serving Chinese to a Japanese reader looks like a bug on their end. """ from __future__ import annotations import pytest from tests.test_security_awareness.conftest import ( ADMIN_BASE, SELF_BASE, article_payload, comic_payload, ) pytestmark = pytest.mark.asyncio async def _publish(client, headers, payload) -> str: r = await client.post(f"{ADMIN_BASE}/materials", json=payload, headers=headers) assert r.status_code == 201, r.text return r.json()["data"]["id"] async def test_reader_gets_their_language_when_it_exists( client, security_officer, reader, ): officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) zh = (await client.get( f"{SELF_BASE}/materials?lang=zh", headers=rh, )).json()["data"][0] assert zh["title"] == "识破钓鱼邮件" assert zh["lang"] == "zh" assert zh["is_fallback_lang"] is False en = (await client.get( f"{SELF_BASE}/materials?lang=en", headers=rh, )).json()["data"][0] assert en["title"] == "Spotting a phishing mail" assert en["lang"] == "en" assert en["is_fallback_lang"] is False async def test_japanese_falls_back_to_english_not_chinese_and_says_so( client, security_officer, reader, ): """ja → en → zh. The Japan office reads English security material; a Japanese speaker shown Chinese has no way to tell whether it was a fallback or the only copy that exists — hence the flag.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) ja = (await client.get( f"{SELF_BASE}/materials?lang=ja", headers=rh, )).json()["data"][0] assert ja["lang"] == "en" assert ja["is_fallback_lang"] is True async def test_zh_only_material_reaches_an_english_reader( client, security_officer, reader, ): officer, oh = security_officer _p, rh = reader await _publish(client, oh, article_payload()) row = (await client.get( f"{SELF_BASE}/materials?lang=en", headers=rh, )).json()["data"][0] assert row["lang"] == "zh" assert row["is_fallback_lang"] is True async def test_explicit_lang_beats_the_accept_language_header( client, security_officer, reader, ): """Helix's own switcher is what the reader actually chose.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) row = (await client.get( f"{SELF_BASE}/materials?lang=en", headers={**rh, "Accept-Language": "zh-CN,zh;q=0.9"}, )).json()["data"][0] assert row["lang"] == "en" # And with no explicit param the header is honoured. row = (await client.get( f"{SELF_BASE}/materials", headers={**rh, "Accept-Language": "en-US,en;q=0.9"}, )).json()["data"][0] assert row["lang"] == "en" async def test_unknown_language_degrades_to_the_baseline_instead_of_422( client, security_officer, reader, ): """A reader with an exotic Accept-Language should get content.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, article_payload()) r = await client.get(f"{SELF_BASE}/materials?lang=fr-CA", headers=rh) assert r.status_code == 200 assert r.json()["data"][0]["lang"] == "zh" async def test_search_is_cross_language(client, security_officer, reader): """An employee on the English UI still finds a Chinese-titled item, and vice versa — the haystack is every language plus the tags.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) await _publish(client, oh, article_payload()) hits = (await client.get( f"{SELF_BASE}/materials?q=phishing&lang=zh", headers=rh, )).json()["data"] assert [h["title"] for h in hits] == ["识破钓鱼邮件"] hits = (await client.get( f"{SELF_BASE}/materials?q=口令&lang=en", headers=rh, )).json()["data"] assert len(hits) == 1 async def test_multiple_tokens_narrow(client, security_officer, reader): officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) assert len((await client.get( f"{SELF_BASE}/materials?q=phishing 钓鱼", headers=rh, )).json()["data"]) == 1 assert len((await client.get( f"{SELF_BASE}/materials?q=phishing 口令", headers=rh, )).json()["data"]) == 0 async def test_category_and_tag_filters(client, security_officer, reader): officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) await _publish(client, oh, article_payload()) assert len((await client.get( f"{SELF_BASE}/materials?category=phishing", headers=rh, )).json()["data"]) == 1 assert len((await client.get( f"{SELF_BASE}/materials?tag=phishing", headers=rh, )).json()["data"]) == 1 assert len((await client.get( f"{SELF_BASE}/materials?category=data", headers=rh, )).json()["data"]) == 0 async def test_facets_come_from_the_whole_library_not_the_filtered_result( client, security_officer, reader, ): """Chips computed off an already-filtered list vanish the moment you use one, which reads as the app losing options.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, comic_payload(officer.id)) await _publish(client, oh, article_payload()) facets = (await client.get(f"{SELF_BASE}/facets", headers=rh)).json()["data"] counts = {c["key"]: c["count"] for c in facets["categories"]} assert counts["phishing"] == 1 assert counts["account"] == 1 assert facets["total"] == 2 assert facets["unread"] == 2 # Every catalog category is offered with a count, so the UI can hide # the empty ones itself rather than being handed a truncated list. assert len(facets["categories"]) >= 8 async def test_unread_count_drops_when_the_reader_opens_one( client, security_officer, reader, ): officer, oh = security_officer _p, rh = reader mid = await _publish(client, oh, article_payload()) assert (await client.get( f"{SELF_BASE}/facets", headers=rh, )).json()["data"]["unread"] == 1 await client.get(f"{SELF_BASE}/materials/{mid}", headers=rh) facets = (await client.get(f"{SELF_BASE}/facets", headers=rh)).json()["data"] assert facets["unread"] == 0 card = (await client.get( f"{SELF_BASE}/materials", headers=rh, )).json()["data"][0] assert card["viewed_at"] is not None async def test_read_state_is_per_person(client, db_session, security_officer, reader): """One reader opening a material must not mark it read for everybody.""" from tests.test_security_awareness.conftest import headers_for, make_person officer, oh = security_officer _p, rh = reader other = await make_person(db_session, "other@test.com", role="employee") await db_session.commit() oth = headers_for(other, ["employee"]) mid = await _publish(client, oh, article_payload()) await client.get(f"{SELF_BASE}/materials/{mid}", headers=rh) assert (await client.get( f"{SELF_BASE}/facets", headers=oth, )).json()["data"]["unread"] == 1 async def test_pinned_material_sorts_first(client, security_officer, reader): officer, oh = security_officer _p, rh = reader first = await _publish(client, oh, article_payload()) second = await _publish(client, oh, comic_payload(officer.id)) ids = [m["id"] for m in (await client.get( f"{SELF_BASE}/materials", headers=rh, )).json()["data"]] assert ids == [first, second] await client.put( f"{ADMIN_BASE}/materials/{second}", json={"is_pinned": True}, headers=oh, ) ids = [m["id"] for m in (await client.get( f"{SELF_BASE}/materials", headers=rh, )).json()["data"]] assert ids == [second, first] async def test_detail_exposes_the_available_language_list( client, security_officer, reader, ): """So the detail page can offer a switch when the reader wants the original rather than the fallback they were handed.""" officer, oh = security_officer _p, rh = reader mid = await _publish(client, oh, comic_payload(officer.id)) detail = (await client.get( f"{SELF_BASE}/materials/{mid}?lang=ja", headers=rh, )).json()["data"] assert sorted(detail["available_langs"]) == ["en", "zh"] assert detail["lang"] == "en" assert detail["is_fallback_lang"] is True async def test_estimated_minutes_is_derived_and_absent_for_links( client, security_officer, reader, ): """We have no idea how long somebody else's page takes to read, and inventing a number erodes trust in every other number on the page.""" officer, oh = security_officer _p, rh = reader await _publish(client, oh, article_payload(body="字" * 1200)) await _publish(client, oh, { "kind": "link", "category": "policy", "publish": True, "localized": {"zh": { "title": "安全红线", "link_url": "https://example.invalid/x", }}, }) by_title = { m["title"]: m for m in (await client.get( f"{SELF_BASE}/materials", headers=rh, )).json()["data"] } assert by_title["口令怎么设"]["estimated_minutes"] == 3 assert by_title["安全红线"]["estimated_minutes"] is None async def test_admin_override_wins_over_the_estimate( client, security_officer, reader, ): officer, oh = security_officer _p, rh = reader payload = article_payload(body="字" * 1200) payload["estimated_minutes"] = 9 await _publish(client, oh, payload) card = (await client.get( f"{SELF_BASE}/materials", headers=rh, )).json()["data"][0] assert card["estimated_minutes"] == 9