Scope mobile Search by repository
This commit is contained in:
1 parent
7636e30482
commit
ec86bf55a8
15 files changed
+234
-25
No files matched your search
@@ -18,6 +18,7 @@ def test_api_requests_resolve_inside_dashboard_subpath():
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications/",
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications?page=",
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/notifications/read",
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/repositories/search?q=",
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/search?q=",
|
||||
"https://forge.alexanderwhitestone.com/dashboard/api/v1/work/",
|
||||
}
|
||||
@@ -71,6 +71,42 @@ def test_mobile_search_renders_touch_sized_type_and_status_scope_controls():
|
||||
assert "'&state=' + encodeURIComponent(scope.state)" in html
|
||||
|
||||
|
||||
def test_mobile_search_exposes_accessible_repository_scope_and_forwards_it():
|
||||
html = dashboard_bundle_text()
|
||||
css = (FRONTEND / "dashboard.css").read_text()
|
||||
|
||||
assert '<label for="cmd-search-repository">Repository</label>' in html
|
||||
assert '<input id="cmd-search-repository"' in html
|
||||
assert 'list="cmd-search-repositories"' in html
|
||||
assert '<datalist id="cmd-search-repositories">' in html
|
||||
assert "api/v1/repositories/search?q=" in html
|
||||
assert "repository:qs('#cmd-search-repository').value" in html
|
||||
assert "'&repository=' + encodeURIComponent(scope.repository)" in html
|
||||
assert '#cmd-search-repository' in css
|
||||
|
||||
|
||||
def test_remote_command_search_repository_change_aborts_and_restarts_scoped_search():
|
||||
script = f"""
|
||||
const filterCommands = require({json.dumps(str(COMMANDS))});
|
||||
(async () => {{
|
||||
const pending = [];
|
||||
const controller = filterCommands.createGlobalSearchController({{
|
||||
delay: 0,
|
||||
search: (query, signal, page, scope) => new Promise(resolve => pending.push({{signal, scope, resolve}})),
|
||||
onState() {{}},
|
||||
}});
|
||||
controller.setQuery('mobile');
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
controller.setScope({{kind:'all', state:'all', repository:'stackchain/api'}});
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
if (!pending[0].signal.aborted) throw new Error('repository change did not abort old request');
|
||||
if (pending[1].scope.repository !== 'stackchain/api') throw new Error('repository was not forwarded');
|
||||
pending[1].resolve({{items:[],has_more:false,next_page:2}});
|
||||
}})().catch(error => {{ console.error(error); process.exit(1); }});
|
||||
"""
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
|
||||
|
||||
|
||||
def test_remote_command_search_ignores_stale_responses():
|
||||
script = f"""
|
||||
const filterCommands = require({json.dumps(str(COMMANDS))});
|
||||
|
||||
@@ -925,6 +925,7 @@ async def test_anonymous_search_preview_preserves_only_valid_search_scope(access
|
||||
valid = await client.get("/", params={
|
||||
"search": "release blocker", "preview": "pull:stackchain/api:42",
|
||||
"search_kind": "pull", "search_state": "open",
|
||||
"search_repository": "stackchain/api",
|
||||
})
|
||||
invalid = await client.get("/", params={
|
||||
"search": "release blocker", "preview": "pull:stackchain/api:42",
|
||||
@@ -935,7 +936,7 @@ async def test_anonymous_search_preview_preserves_only_valid_search_scope(access
|
||||
invalid_continue = parse_qs(urlsplit(invalid.headers["location"]).query)["continue"][0]
|
||||
assert valid_continue.endswith(
|
||||
"search=release+blocker&preview=pull%3Astackchain%2Fapi%3A42&"
|
||||
"search_kind=pull&search_state=open"
|
||||
"search_kind=pull&search_state=open&search_repository=stackchain%2Fapi"
|
||||
)
|
||||
assert invalid_continue == (
|
||||
"./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A42"
|
||||
|
||||
@@ -6,6 +6,68 @@ import pytest
|
||||
from src import gitea_proxy, main
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_endpoint_scopes_every_page_to_an_exact_repository(monkeypatch):
|
||||
requested = []
|
||||
|
||||
async def search(query, limit, page, kind, state, repository):
|
||||
requested.append((query, limit, page, kind, state, repository))
|
||||
return {"items": [], "partial": False, "has_more": False, "next_page": 4}
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "global_search", search, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
"/api/v1/search?q=mobile&limit=7&page=3&kind=pull&state=open&repository=stackchain%2Fapi"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert requested == [("mobile", 7, 3, "pull", "open", "stackchain/api")]
|
||||
assert response.json()["scope"] == {
|
||||
"kind": "pull", "state": "open", "repository": "stackchain/api"
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_rejects_malformed_repository_scope_before_upstream(monkeypatch):
|
||||
called = False
|
||||
|
||||
async def search(*args):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(main.gitea_proxy, "global_search", search, raising=False)
|
||||
transport = httpx.ASGITransport(app=main.app)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
"/api/v1/search?q=mobile&repository=stackchain%2Fapi%2Fsecrets"
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert called is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_repository_scope_reaches_issue_and_pull_streams():
|
||||
requests = []
|
||||
|
||||
async def handler(request):
|
||||
requests.append(dict(request.url.params))
|
||||
return httpx.Response(200, json=[])
|
||||
|
||||
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
||||
try:
|
||||
await gitea_proxy.global_search(
|
||||
"mobile", limit=6, page=2, repository="stackchain/api"
|
||||
)
|
||||
finally:
|
||||
await gitea_proxy.stop_client()
|
||||
|
||||
assert len(requests) == 2
|
||||
assert all(request["owner"] == "stackchain" and request["repo"] == "api" for request in requests)
|
||||
assert all(request["page"] == "2" for request in requests)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_global_search_endpoint_forwards_valid_type_and_status_scope(monkeypatch):
|
||||
requested = []
|
||||
|
||||
@@ -257,7 +257,7 @@ async function destination(continuation) {{
|
||||
return replaced;
|
||||
}}
|
||||
(async () => process.stdout.write(JSON.stringify({{
|
||||
valid:await destination('./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9'),
|
||||
valid:await destination('./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9&search_kind=pull&search_state=open&search_repository=stackchain%2Fapi'),
|
||||
malformed:await destination('./?search=release&preview=issue%3Astackchain%2Fapi%3A0'),
|
||||
unknown:await destination('./?search=release&next=https%3A%2F%2Fevil.example'),
|
||||
}})))().catch(error => {{ console.error(error); process.exit(1); }});
|
||||
@@ -266,7 +266,7 @@ async function destination(continuation) {{
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == {
|
||||
"valid": "./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9",
|
||||
"valid": "./?search=release+blocker&preview=pull%3Astackchain%2Fapi%3A9&search_kind=pull&search_state=open&search_repository=stackchain%2Fapi",
|
||||
"malformed": "./",
|
||||
"unknown": "./",
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ process.stdout.write(JSON.stringify({{
|
||||
assert payload["oversized"]["state"] == {"kind": None}
|
||||
|
||||
|
||||
def test_search_history_round_trips_only_bounded_type_and_status_scope():
|
||||
def test_search_history_round_trips_bounded_type_status_and_repository_scope():
|
||||
script = f"""
|
||||
const createTaskOverlayHistory = require({json.dumps(str(OVERLAY_HISTORY))});
|
||||
function restore(search) {{
|
||||
@@ -306,11 +306,11 @@ const history = {{state:null, pushState(state,title,url) {{this.state=state; thi
|
||||
const controller = createTaskOverlayHistory({{
|
||||
history, location, eventTarget:{{addEventListener() {{}}}}, onChange() {{}},
|
||||
}});
|
||||
controller.open('search', {{query:'mobile', scope:{{kind:'pull',state:'open'}}}});
|
||||
controller.open('search', {{query:'mobile', scope:{{kind:'pull',state:'open',repository:'stackchain/api'}}}});
|
||||
process.stdout.write(JSON.stringify({{
|
||||
opened:controller.currentState(), url:history.url,
|
||||
valid:restore('?search=mobile&search_kind=issue&search_state=closed'),
|
||||
invalid:restore('?search=mobile&search_kind=script&search_state=secret'),
|
||||
valid:restore('?search=mobile&search_kind=issue&search_state=closed&search_repository=stackchain%2Fapi'),
|
||||
invalid:restore('?search=mobile&search_kind=script&search_state=secret&search_repository=stackchain%2Fapi%2Fprivate'),
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
@@ -318,11 +318,20 @@ process.stdout.write(JSON.stringify({{
|
||||
assert result.returncode == 0, result.stderr
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["opened"] == {
|
||||
"kind": "search", "query": "mobile", "scope": {"kind": "pull", "state": "open"}
|
||||
"kind": "search", "query": "mobile", "scope": {
|
||||
"kind": "pull", "state": "open", "repository": "stackchain/api"
|
||||
}
|
||||
}
|
||||
assert payload["url"] == (
|
||||
"/dashboard/?search=mobile&search_kind=pull&search_state=open&"
|
||||
"search_repository=stackchain%2Fapi"
|
||||
)
|
||||
assert payload["valid"]["state"]["scope"] == {
|
||||
"kind": "issue", "state": "closed", "repository": "stackchain/api"
|
||||
}
|
||||
assert payload["invalid"]["state"]["scope"] == {
|
||||
"kind": "all", "state": "all", "repository": ""
|
||||
}
|
||||
assert payload["url"] == "/dashboard/?search=mobile&search_kind=pull&search_state=open"
|
||||
assert payload["valid"]["state"]["scope"] == {"kind": "issue", "state": "closed"}
|
||||
assert payload["invalid"]["state"]["scope"] == {"kind": "all", "state": "all"}
|
||||
|
||||
|
||||
def test_dashboard_persists_search_query_and_restores_canonical_preview():
|
||||
|
||||
Reference in new issue
Block a user