feat(api): standardize REST contracts and auth

This commit is contained in:
2026-07-30 20:38:51 +08:00
parent ae1a657554
commit ba947b616b
86 changed files with 54193 additions and 990 deletions
+24 -54
View File
@@ -32,24 +32,18 @@ def test_load_auth_context_supports_aliases(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_PROJECT_ID", "p1")
monkeypatch.setenv("TJWATER_USERNAME", "tester")
monkeypatch.setenv("TJWATER_NETWORK", "net1")
auth = core.load_auth_context(auth_stdin=False)
assert auth.server == "http://server"
assert auth.access_token == "abc"
assert auth.project_id == "p1"
assert auth.username == "tester"
assert auth.network == "net1"
def test_build_runtime_context_uses_default_server(monkeypatch):
monkeypatch.delenv("TJWATER_SERVER", raising=False)
monkeypatch.delenv("TJWATER_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("TJWATER_PROJECT_ID", raising=False)
monkeypatch.delenv("TJWATER_USERNAME", raising=False)
monkeypatch.delenv("TJWATER_NETWORK", raising=False)
monkeypatch.delenv("TJWATER_EXTRA_HEADERS", raising=False)
runtime = core.build_runtime_context(
@@ -68,7 +62,7 @@ def test_auth_stdin_can_be_reused_with_runtime_context_cache(monkeypatch):
def fake_request_json(ctx, **kwargs):
observed_runtime_ids.append(id(ctx))
assert ctx.auth.access_token == "token-1"
assert kwargs["params"] == {"network": "tjwater", "junction": "11"}
assert kwargs["params"] == {"junction": "11"}
return {"id": "11"}, 5
monkeypatch.setattr(common, "request_json", fake_request_json)
@@ -81,7 +75,6 @@ def test_auth_stdin_can_be_reused_with_runtime_context_cache(monkeypatch):
"server": "http://server",
"access_token": "token-1",
"project_id": "project-1",
"network": "tjwater",
}
),
)
@@ -105,7 +98,6 @@ def test_network_get_junction_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-junction-properties", "--junction", "J1"])
@@ -116,8 +108,8 @@ def test_network_get_junction_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "J1"}
assert captured == {
"access_token": "abc",
"path": "/getjunctionproperties/",
"params": {"network": "tjwater", "junction": "J1"},
"path": "/junctions/properties",
"params": {"junction": "J1"},
}
@@ -132,7 +124,6 @@ def test_network_get_pipe_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-pipe-properties", "--pipe", "P1"])
@@ -143,8 +134,8 @@ def test_network_get_pipe_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "P1"}
assert captured == {
"access_token": "abc",
"path": "/getpipeproperties/",
"params": {"network": "tjwater", "pipe": "P1"},
"path": "/pipes/properties",
"params": {"pipe": "P1"},
}
@@ -159,7 +150,6 @@ def test_network_get_all_pipes_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-all-pipes-properties"])
@@ -170,8 +160,8 @@ def test_network_get_all_pipes_properties_uses_network_context(monkeypatch):
assert payload["data"] == [{"id": "P1"}]
assert captured == {
"access_token": "abc",
"path": "/getallpipeproperties/",
"params": {"network": "tjwater"},
"path": "/pipes",
"params": {},
}
@@ -186,7 +176,6 @@ def test_network_get_reservoir_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-reservoir-properties", "--reservoir", "R1"])
@@ -197,8 +186,8 @@ def test_network_get_reservoir_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "R1"}
assert captured == {
"access_token": "abc",
"path": "/getreservoirproperties/",
"params": {"network": "tjwater", "reservoir": "R1"},
"path": "/reservoirs/properties",
"params": {"reservoir": "R1"},
}
@@ -213,7 +202,6 @@ def test_network_get_all_reservoir_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-all-reservoirs-properties"])
@@ -224,8 +212,8 @@ def test_network_get_all_reservoir_properties_uses_network_context(monkeypatch):
assert payload["data"] == [{"id": "R1"}]
assert captured == {
"access_token": "abc",
"path": "/getallreservoirproperties/",
"params": {"network": "tjwater"},
"path": "/reservoirs",
"params": {},
}
@@ -240,7 +228,6 @@ def test_network_get_tank_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-tank-properties", "--tank", "T1"])
@@ -251,8 +238,8 @@ def test_network_get_tank_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "T1"}
assert captured == {
"access_token": "abc",
"path": "/gettankproperties/",
"params": {"network": "tjwater", "tank": "T1"},
"path": "/tanks/properties",
"params": {"tank": "T1"},
}
@@ -267,7 +254,6 @@ def test_network_get_all_tank_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-all-tanks-properties"])
@@ -278,8 +264,8 @@ def test_network_get_all_tank_properties_uses_network_context(monkeypatch):
assert payload["data"] == [{"id": "T1"}]
assert captured == {
"access_token": "abc",
"path": "/getalltankproperties/",
"params": {"network": "tjwater"},
"path": "/tanks",
"params": {},
}
@@ -294,7 +280,6 @@ def test_network_get_pump_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-pump-properties", "--pump", "PU1"])
@@ -305,8 +290,8 @@ def test_network_get_pump_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "PU1"}
assert captured == {
"access_token": "abc",
"path": "/getpumpproperties/",
"params": {"network": "tjwater", "pump": "PU1"},
"path": "/pumps/properties",
"params": {"pump": "PU1"},
}
@@ -321,7 +306,6 @@ def test_network_get_all_pump_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-all-pumps-properties"])
@@ -332,8 +316,8 @@ def test_network_get_all_pump_properties_uses_network_context(monkeypatch):
assert payload["data"] == [{"id": "PU1"}]
assert captured == {
"access_token": "abc",
"path": "/getallpumpproperties/",
"params": {"network": "tjwater"},
"path": "/pumps",
"params": {},
}
@@ -348,7 +332,6 @@ def test_network_get_valve_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-valve-properties", "--valve", "V1"])
@@ -359,8 +342,8 @@ def test_network_get_valve_properties_uses_network_context(monkeypatch):
assert payload["data"] == {"id": "V1"}
assert captured == {
"access_token": "abc",
"path": "/getvalveproperties/",
"params": {"network": "tjwater", "valve": "V1"},
"path": "/valves/properties",
"params": {"valve": "V1"},
}
@@ -375,7 +358,6 @@ def test_network_get_all_valve_properties_uses_network_context(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "tjwater")
monkeypatch.setattr(common, "request_json", fake_request_json)
result = runner.invoke(app, ["network", "get-all-valves-properties"])
@@ -386,8 +368,8 @@ def test_network_get_all_valve_properties_uses_network_context(monkeypatch):
assert payload["data"] == [{"id": "V1"}]
assert captured == {
"access_token": "abc",
"path": "/getallvalveproperties/",
"params": {"network": "tjwater"},
"path": "/valves",
"params": {},
}
@@ -525,7 +507,6 @@ def test_realtime_property_help_lists_supported_fields():
def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
burst_path = tmp_path / "burst.json"
burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8")
@@ -559,7 +540,6 @@ def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path:
def test_analysis_contaminant_sends_required_scheme_name(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
captured = {}
def fake_request(**kwargs):
@@ -588,7 +568,6 @@ def test_analysis_contaminant_sends_required_scheme_name(monkeypatch):
assert result.exit_code == 0
assert captured["params"] == {
"network": "demo",
"start_time": "2025-01-02T03:04:05+08:00",
"source": "N1",
"concentration": 10.0,
@@ -600,7 +579,6 @@ def test_analysis_contaminant_sends_required_scheme_name(monkeypatch):
def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Path):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
captured = {}
valve_path = tmp_path / "valve.json"
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
@@ -633,11 +611,10 @@ def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Pat
assert result.exit_code == 0
assert captured["params"] == {
"network": "demo",
"start_time": "2025-01-02T03:04:05+08:00",
"valves": ["V1"],
"valves_k": [0.5],
"drainage_node_ID": "N1",
"drainage_node_id": "N1",
"flush_flow": 100.0,
"duration": 900,
"scheme_name": "flush_case_01",
@@ -647,7 +624,6 @@ def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Pat
def test_analysis_valve_close_sends_required_scheme_name(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
captured = {}
def fake_request(**kwargs):
@@ -676,7 +652,6 @@ def test_analysis_valve_close_sends_required_scheme_name(monkeypatch):
assert result.exit_code == 0
assert captured["params"] == {
"network": "demo",
"start_time": "2025-01-02T03:04:05+08:00",
"valves": ["V1"],
"duration": 900,
@@ -687,7 +662,6 @@ def test_analysis_valve_close_sends_required_scheme_name(monkeypatch):
def test_analysis_contaminant_requires_scheme(monkeypatch, capsys):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
exit_code = main(
[
@@ -713,7 +687,6 @@ def test_analysis_contaminant_requires_scheme(monkeypatch, capsys):
def test_analysis_flushing_requires_scheme(monkeypatch, tmp_path: Path, capsys):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
valve_path = tmp_path / "valve.json"
valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8")
@@ -741,7 +714,6 @@ def test_analysis_flushing_requires_scheme(monkeypatch, tmp_path: Path, capsys):
def test_analysis_valve_close_requires_scheme(monkeypatch, capsys):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
exit_code = main(
[
@@ -921,7 +893,6 @@ def test_main_bare_analysis_returns_typer_help_without_json_error(capsys):
def test_simulation_run_translates_rfc3339(monkeypatch):
monkeypatch.setenv("TJWATER_SERVER", "http://server")
monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc")
monkeypatch.setenv("TJWATER_NETWORK", "demo")
captured = {}
def fake_request(**kwargs):
@@ -944,7 +915,6 @@ def test_simulation_run_translates_rfc3339(monkeypatch):
assert result.exit_code == 0
assert captured["json"] == {
"name": "demo",
"start_time": "2025-01-02T03:04:05+08:00",
"duration": 30,
}