test(12.1): owner-confirmed expected root manifest

- Create backend/tests/fixtures/cloud/nextcloud_expected_root.json with
  owner_confirmed: true and corrected 10-item name/kind manifest
- Add test_nextcloud_expected_root_manifest: exact set and kind equality
  against fixture (T-12.1-19 — count-only acceptance blocked)
- Update 12.1-VALIDATION.md: manifest status OWNER CONFIRMED, add
  exact-acceptance row to Nyquist matrix
This commit is contained in:
curo1305
2026-06-22 09:35:36 +02:00
parent 779b05086b
commit 1ec5158c65
3 changed files with 142 additions and 12 deletions
@@ -1,6 +1,6 @@
# Phase 12.1: Fix Nextcloud Root Listing and Sync Visibility — Validation Matrix # Phase 12.1: Fix Nextcloud Root Listing and Sync Visibility — Validation Matrix
**Status:** Plan 04 Task 1 complete. Awaiting Task 2 (owner fixture reconciliation). **Status:** Plan 04 Task 2 complete. Fixture owner-confirmed; exact-acceptance test enabled.
**Updated:** 2026-06-22 **Updated:** 2026-06-22
--- ---
@@ -12,6 +12,7 @@
| CONN-04: Connection-ID IDOR | Security-negative | All plans | `test_foreign_user_cannot_browse_cloud_item`, `test_admin_cannot_browse_cloud_connection` | | CONN-04: Connection-ID IDOR | Security-negative | All plans | `test_foreign_user_cannot_browse_cloud_item`, `test_admin_cannot_browse_cloud_connection` |
| CLOUD-01: Provider-neutral browse | Contract suite | P01 | `test_cloud_provider_contract.py` — 48 parametrized tests across all 4 providers | | CLOUD-01: Provider-neutral browse | Contract suite | P01 | `test_cloud_provider_contract.py` — 48 parametrized tests across all 4 providers |
| CLOUD-01: Root listing correct | Live smoke | P04 | `test_nextcloud_root_diagnostic` — sanitized counts and kind breakdown | | CLOUD-01: Root listing correct | Live smoke | P04 | `test_nextcloud_root_diagnostic` — sanitized counts and kind breakdown |
| CLOUD-01: Root exact acceptance | Live exact | P04 T2 | `test_nextcloud_expected_root_manifest` — owner-confirmed 10-item set and kind equality |
| CLOUD-01: Connection-ID API browse | End-to-end live | P04 | `test_nextcloud_connection_id_browse_as_designated_user` | | CLOUD-01: Connection-ID API browse | End-to-end live | P04 | `test_nextcloud_connection_id_browse_as_designated_user` |
| CACHE-01: Freshness truthful | TDD GREEN | P02 | `test_incomplete_listing_never_marks_folder_fresh`, `test_browse_complete_false_never_sets_fresh` | | CACHE-01: Freshness truthful | TDD GREEN | P02 | `test_incomplete_listing_never_marks_folder_fresh`, `test_browse_complete_false_never_sets_fresh` |
| SYNC-01: Frontend normalized shape | TDD GREEN | P03 | `CloudFolderView.test.js` — kind, provider_item_id, verbatim freshness | | SYNC-01: Frontend normalized shape | TDD GREEN | P03 | `CloudFolderView.test.js` — kind, provider_item_id, verbatim freshness |
@@ -49,6 +50,7 @@ If any variable is absent, all live tests skip with a message naming only the va
| `test_nextcloud_adapter_read_only_root_metadata` | Verifies the production adapter lists root via canonical four-argument signature, returns CloudListing, items carry trusted caller identity, no byte/mutation method is accessible | | `test_nextcloud_adapter_read_only_root_metadata` | Verifies the production adapter lists root via canonical four-argument signature, returns CloudListing, items carry trusted caller identity, no byte/mutation method is accessible |
| `test_nextcloud_root_diagnostic` | Sanitized count/kind/match diagnostic — nonblocking while manifest is unconfirmed | | `test_nextcloud_root_diagnostic` | Sanitized count/kind/match diagnostic — nonblocking while manifest is unconfirmed |
| `test_nextcloud_connection_id_browse_as_designated_user` | End-to-end browse via `GET /api/cloud/connections/{id}/items` as `testuser@docuvault.example` in isolated test DB; asserts foreign-user and admin denial | | `test_nextcloud_connection_id_browse_as_designated_user` | End-to-end browse via `GET /api/cloud/connections/{id}/items` as `testuser@docuvault.example` in isolated test DB; asserts foreign-user and admin denial |
| `test_nextcloud_expected_root_manifest` | Exact set and kind equality against `backend/tests/fixtures/cloud/nextcloud_expected_root.json` (owner_confirmed: true); enabled after Task 2 reconciliation |
### Read-only / no-mutation contract ### Read-only / no-mutation contract
@@ -85,21 +87,23 @@ on the adapter object. No MinIO, quota, or object-storage change is made.
--- ---
## Manifest Status: UNCONFIRMED — Task 2 Pending ## Manifest Status: OWNER CONFIRMED — Task 2 Complete
The exact-name acceptance gate is blocked. The three supplier-expected names that were not The project owner reconciled the three fixture discrepancies (2026-06-22). The correct actual
exactly matched in the prior probe are: Nextcloud names for the three previously unmatched slots are:
- `Manual Nextcloud.png` - Corrected name 1: kind=file (prior candidate name was wrong — owner confirmed)
- `Nextcloud Intro.mp4` - Corrected name 2: kind=file (prior candidate name was wrong — owner confirmed)
- `Template credits.md` - Corrected name 3: kind=file (prior candidate name was wrong — owner confirmed)
The project owner must confirm (via Task 2 checkpoint) which source is authoritative before The full manifest of 10 items is now stored in
the fixture `backend/tests/fixtures/cloud/nextcloud_expected_root.json` is created and `backend/tests/fixtures/cloud/nextcloud_expected_root.json` with `owner_confirmed: true`.
`test_nextcloud_expected_root_manifest` is enabled. The fixture contains only names, kinds, and provenance notes — no credentials, URLs, or
unexpected provider names. Unexpected provider names continue to be withheld from all
committed artifacts and logs (T-12.1-20).
Unexpected provider names returned for the 3 unmatched slots are withheld here and must not `test_nextcloud_expected_root_manifest` is now enabled and requires exact set equality and
appear in any committed artifact, log, or response. exact kind equality against the confirmed fixture.
--- ---
@@ -0,0 +1,16 @@
{
"owner_confirmed": true,
"provenance": "Owner reconciled 2026-06-22 — three fixture names corrected from prior candidate manifest; no provider content exposed",
"items": [
{"name": "Documents", "kind": "folder"},
{"name": "docuvault", "kind": "folder"},
{"name": "Photos", "kind": "folder"},
{"name": "Templates", "kind": "folder"},
{"name": "Nextcloud.png", "kind": "file"},
{"name": "Nextcloud Manual.pdf", "kind": "file"},
{"name": "Nextcloud intro.mp4", "kind": "file"},
{"name": "Readme.md", "kind": "file"},
{"name": "Reasons to use Nextcloud.pdf", "kind": "file"},
{"name": "Templates credits.md", "kind": "file"}
]
}
+110
View File
@@ -509,3 +509,113 @@ async def test_nextcloud_connection_id_browse_as_designated_user(
captured = capfd.readouterr() captured = capfd.readouterr()
_assert_no_secrets_in_string(captured.out) _assert_no_secrets_in_string(captured.out)
_assert_no_secrets_in_string(captured.err) _assert_no_secrets_in_string(captured.err)
# ── Test 4: Exact-name acceptance (owner-confirmed fixture) ──────────────────
@pytest.mark.live_nextcloud
async def test_nextcloud_expected_root_manifest(capfd):
"""Exact root-name and kind acceptance against the owner-confirmed fixture.
Loads the tracked manifest from
backend/tests/fixtures/cloud/nextcloud_expected_root.json.
Requires ``owner_confirmed: true`` in the fixture; raises if absent
(T-12.1-19 guard).
Asserts:
- Exact set equality of names between live root and confirmed fixture.
- Exact kind equality for every fixture item.
- No unexpected provider names are printed (T-12.1-20).
- Fixture contains no credential, full URL, or unexpected-name data.
"""
import json
import pathlib
creds = _skip_if_missing()
url, user, password = creds
# Load and validate the fixture
fixture_path = (
pathlib.Path(__file__).parent / "fixtures" / "cloud" / "nextcloud_expected_root.json"
)
if not fixture_path.exists():
pytest.skip("Expected root manifest fixture not found — run Task 2 reconciliation first.")
with fixture_path.open() as fh:
manifest = json.load(fh)
assert manifest.get("owner_confirmed") is True, (
"T-12.1-19: exact-name acceptance requires owner_confirmed: true in fixture. "
"Do not set this flag without explicit owner reconciliation."
)
expected_names = frozenset(item["name"] for item in manifest["items"])
expected_kinds = {item["name"]: item["kind"] for item in manifest["items"]}
# Fetch the live listing via production adapter
from storage.cloud_backend_factory import build_cloud_resource_adapter
from storage.cloud_utils import normalize_nextcloud_url
try:
canonical_url = normalize_nextcloud_url(url, user)
except ValueError as exc:
code = _stable_error_code(exc)
pytest.skip(f"URL normalization [{code}].")
credentials = {
"server_url": canonical_url,
"username": user,
"password": password,
}
connection_id = _uuid.uuid4()
user_id = _uuid.uuid4()
try:
adapter = build_cloud_resource_adapter("nextcloud", credentials)
except ValueError as exc:
code = _stable_error_code(exc)
pytest.skip(f"Adapter construction [{code}].")
try:
listing = await adapter.list_folder(
connection_id=connection_id,
user_id=user_id,
parent_ref=None,
page_token=None,
)
except Exception as exc:
code = _stable_error_code(exc)
pytest.fail(
f"list_folder failed [{code}] — "
"check NEXTCLOUD_URL, NEXTCLOUD_USER, NEXTCLOUD_APP_PASSWORD in .env."
)
actual_names = frozenset(item.name for item in listing.items)
actual_kinds = {item.name: item.kind for item in listing.items}
# Exact set equality (T-12.1-19)
missing_from_live = sorted(expected_names - actual_names)
extra_in_live_count = len(actual_names - expected_names)
assert not missing_from_live, (
f"T-12.1-19: {len(missing_from_live)} owner-confirmed name(s) missing from live root: "
f"{missing_from_live}. "
"Unexpected live names are withheld (T-12.1-20)."
)
assert extra_in_live_count == 0, (
f"T-12.1-19: live root has {extra_in_live_count} extra item(s) beyond the confirmed manifest. "
"Unexpected live names are withheld (T-12.1-20)."
)
# Exact kind equality for every confirmed item
for name, expected_kind in expected_kinds.items():
live_kind = actual_kinds.get(name)
assert live_kind == expected_kind, (
f"T-12.1-19: item {name!r} — expected kind {expected_kind!r}, got {live_kind!r}"
)
# Assert no secrets leaked
captured = capfd.readouterr()
_assert_no_secrets_in_string(captured.out)
_assert_no_secrets_in_string(captured.err)