This is an automated email from the ASF dual-hosted git repository. lahirujayathilake pushed a commit to branch access-integration-v3 in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit ea23690bc2ef33a6cbea56e0c356bf4fd3cf1782 Author: lahiruj <[email protected]> AuthorDate: Wed May 20 19:09:45 2026 -0400 Updated mock AMIE server --- .../AMIE-Processor/mock-server/mock-amie-server.py | 316 ++++++++++++++------- 1 file changed, 207 insertions(+), 109 deletions(-) diff --git a/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py b/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py index 38f509d7e..8b331ddc9 100644 --- a/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py +++ b/connectors/ACCESS/AMIE-Processor/mock-server/mock-amie-server.py @@ -17,16 +17,13 @@ # specific language governing permissions and limitations # under the License. # -# Mock AMIE server for testing the ACCESS CI service with both -# success and failure scenarios. Simulates the AMIE API endpoints -# that the service polls. -# -# Usage: -# python3 mock-amie-server.py -# -# Then point the service at: access.amie.base-url=http://localhost:8180 +# Mock AMIE server for testing the ACCESS-AMIE connector. Generates +# packets that match the canonical AMIE protocol field names (see +# connectors/ACCESS/AMIE-Processor/testdata/*/incoming-request.json for the +# reference shapes). Field names are kept identical to what a real AMIE +# upstream emits so the connector can be exercised end-to-end without a +# decoder shim. -import json import os import random import time @@ -38,11 +35,10 @@ app = Flask(__name__) pending_packets = [] replied_packets = {} -packet_counter = 900000 # starting ID +packet_counter = 900000 stats = {"created": 0, "fetched": 0, "replied": 0} SCENARIOS_DIR = Path(__file__).parent / "scenarios" - DEV_EMAIL = os.getenv("DEV_EMAIL", "").strip() @@ -70,9 +66,12 @@ def make_packet(packet_type, body, transaction_id=None): } -# Scenario generators +# ----- Success generators (canonical AMIE protocol field names) ----- def gen_valid_project_create(): + """request_project_create — site is being asked to create a new project. + Note: AMIE protocol does NOT include ProjectID here; the receiving site + assigns one and returns it in notify_project_create.""" gid = str(random.randint(100000, 999999)) grant = f"TST{random.randint(100000, 999999)}" return make_packet("request_project_create", { @@ -90,15 +89,17 @@ def gen_valid_project_create(): "ServiceUnitsAllocated": str(random.randint(1000, 50000)), "StartDate": "2026-01-01", "EndDate": "2026-12-31", - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], }) def gen_valid_account_create(): + """request_account_create — user requests an account on an existing project.""" gid = str(random.randint(100000, 999999)) return make_packet("request_account_create", { "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", "GrantNumber": f"TST{random.randint(100000, 999999)}", + "UserPersonID": f"person-{uuid.uuid4().hex[:8]}", "UserGlobalID": gid, "UserFirstName": random.choice(["Frank", "Grace", "Heidi", "Ivan", "Judy"]), "UserLastName": random.choice(["Davis", "Garcia", "Rodriguez", "Wilson", "Martinez"]), @@ -107,112 +108,160 @@ def gen_valid_account_create(): "UserOrgCode": "MI", "NsfStatusCode": "GR", "UserDnList": [f"/C=US/O=Mock Institute/CN=User {gid}"], - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], }) def gen_valid_user_modify(): + """request_user_modify — replace user attributes for a known UserGlobalID.""" gid = str(random.randint(100000, 999999)) return make_packet("request_user_modify", { - "PersonID": f"person-mock-{uuid.uuid4().hex[:8]}", "ActionType": "replace", + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "UserPersonID": f"user-person-{uuid.uuid4().hex[:8]}", + "UserGlobalID": gid, "UserFirstName": "Updated", "UserLastName": "Name", "UserEmail": f"updated{gid}@example.edu", "UserOrganization": "Updated University", + "UserOrgCode": "UPD", + "NsfStatusCode": "AC", + }) + + +def gen_valid_data_account_create(): + """data_account_create — pass DNs for an existing user (looked up by GlobalID).""" + gid = str(random.randint(100000, 999999)) + return make_packet("data_account_create", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "GlobalID": gid, + "DnList": [ + f"/C=US/O=Mock Institute/CN=User {gid}", + f"/DC=EDU/CN=user{gid}", + ], + }) + + +def gen_valid_data_project_create(): + """data_project_create — pass DNs for an existing PI (looked up by GlobalID).""" + gid = str(random.randint(100000, 999999)) + return make_packet("data_project_create", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "GlobalID": gid, + "DnList": [ + f"/C=US/O=Mock University/CN=PI {gid}", + ], + }) + + +def gen_valid_project_inactivate(): + return make_packet("request_project_inactivate", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "ResourceList": ["mock-cluster.example.edu"], + "GrantNumber": f"TST{random.randint(100000, 999999)}", + }) + + +def gen_valid_project_reactivate(): + return make_packet("request_project_reactivate", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "ResourceList": ["mock-cluster.example.edu"], + "GrantNumber": f"TST{random.randint(100000, 999999)}", + }) + + +def gen_valid_account_inactivate(): + return make_packet("request_account_inactivate", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "ResourceList": ["mock-cluster.example.edu"], + }) + + +def gen_valid_account_reactivate(): + return make_packet("request_account_reactivate", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "ResourceList": ["mock-cluster.example.edu"], + }) + + +def gen_valid_person_merge(): + primary = str(random.randint(100000, 999999)) + secondary = str(random.randint(100000, 999999)) + return make_packet("request_person_merge", { + "KeepPersonID": f"person-keep-{uuid.uuid4().hex[:8]}", + "DeletePersonID": f"person-delete-{uuid.uuid4().hex[:8]}", + "PrimaryGlobalID": primary, + "SecondaryGlobalID": secondary, + "MergeReason": "Duplicate person records", }) def gen_inform_transaction_complete(): + """Canonical fields: StatusCode, Message, DetailCode.""" return make_packet("inform_transaction_complete", { "StatusCode": "Success", - "StatusMessage": "OK", + "Message": "OK", "DetailCode": "1", }) -# Failure scenarios +# ----- Failure generators (intentionally malformed) ----- def gen_missing_global_id(): - """request_account_create with no UserGlobalID — will cause IllegalArgumentException.""" + """request_account_create with no UserGlobalID.""" return make_packet("request_account_create", { "ProjectID": f"PRJ-FAIL{random.randint(1000, 9999)}", "GrantNumber": f"FAIL{random.randint(100000, 999999)}", - # UserGlobalID MISSING — required field "UserFirstName": "NoGlobalID", "UserLastName": "User", "UserEmail": "[email protected]", - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], }) def gen_missing_grant_number(): - """request_project_create with no GrantNumber — will cause IllegalArgumentException.""" + """request_project_create with no GrantNumber.""" return make_packet("request_project_create", { - # GrantNumber MISSING — required field "PiGlobalID": str(random.randint(100000, 999999)), "PiFirstName": "NoGrant", "PiLastName": "User", "PiEmail": "[email protected]", "NsfStatusCode": "AC", - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], }) -def gen_missing_email(): - """request_project_create with no PiEmail — known optional field.""" - gid = str(random.randint(100000, 999999)) +def gen_missing_pi_global_id(): + """request_project_create with no PiGlobalID.""" return make_packet("request_project_create", { - "GrantNumber": f"NOEMAIL{random.randint(100000, 999999)}", - "PiGlobalID": gid, - "PiFirstName": "NoEmail", + "GrantNumber": f"NOPI{random.randint(100000, 999999)}", + "PiFirstName": "NoGlobal", "PiLastName": "Person", - # PiEmail MISSING — optional per protocol, but we need it - "PiOrganization": "No Email University", - "NsfStatusCode": "AC", - "PiDnList": [], - "ResourceList": "mock-cluster.example.edu", - }) - - -def gen_missing_pi_name(): - """request_project_create with no PiFirstName — will cause IllegalArgumentException.""" - return make_packet("request_project_create", { - "GrantNumber": f"NONAME{random.randint(100000, 999999)}", - "PiGlobalID": str(random.randint(100000, 999999)), - # PiFirstName MISSING — asserted as required - "PiLastName": "OnlyLast", - "PiEmail": "[email protected]", + "PiEmail": "[email protected]", "NsfStatusCode": "AC", - "ResourceList": "mock-cluster.example.edu", - }) - - -def gen_invalid_person_modify(): - """request_user_modify for a person that doesn't exist in the DB.""" - return make_packet("request_user_modify", { - "PersonID": "nonexistent-person-id-12345", - "ActionType": "replace", - "UserFirstName": "Ghost", - "UserLastName": "User", - "UserEmail": "[email protected]", + "ResourceList": ["mock-cluster.example.edu"], }) def gen_unknown_packet_type(): """Packet with an unrecognized type — should hit NoOpHandler.""" - return make_packet("request_something_unknown", { - "SomeField": "SomeValue", - }) + return make_packet("request_something_unknown", {"SomeField": "SomeValue"}) def gen_empty_body(): - """Packet with an empty body — should cause NPE or validation failure.""" return make_packet("request_project_create", {}) -DEV_USER_GID = "100001" +# ----- dev_email scripted scenario ----- +DEV_USER_GID = "100001" DEV_MEMBER_PROJECTS = [ ("DEV-PROJ-002", "Climate Modeling Group", "Alice", "Smith", "[email protected]", "100002", "CoPI"), ("DEV-PROJ-003", "Particle Physics Sim", "Bob", "Johnson", "[email protected]", "100003", "Allocation Manager"), @@ -224,9 +273,7 @@ def gen_dev_email_scenario(): if not DEV_EMAIL: app.logger.warning("DEV_EMAIL is not set; dev_email scenario emits no packets") return [] - packets = [] - packets.append(make_packet("request_project_create", { "GrantNumber": "DEV-PROJ-001", "PfosNumber": "PFOS-DEV-PROJ-001", @@ -242,9 +289,8 @@ def gen_dev_email_scenario(): "ServiceUnitsAllocated": "100000", "StartDate": "2026-01-01", "EndDate": "2026-12-31", - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], })) - for grant, title, pi_first, pi_last, pi_email, pi_gid, dev_role in DEV_MEMBER_PROJECTS: packets.append(make_packet("request_project_create", { "GrantNumber": grant, @@ -261,11 +307,12 @@ def gen_dev_email_scenario(): "ServiceUnitsAllocated": "50000", "StartDate": "2026-01-01", "EndDate": "2026-12-31", - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], })) packets.append(make_packet("request_account_create", { "ProjectID": f"PRJ-{grant}", "GrantNumber": grant, + "UserPersonID": f"person-{DEV_USER_GID}-{grant}", "UserGlobalID": DEV_USER_GID, "UserFirstName": "Dev", "UserLastName": "User", @@ -275,77 +322,135 @@ def gen_dev_email_scenario(): "NsfStatusCode": "AC", "UserDnList": ["/C=US/O=Dev Lab/CN=Dev User"], "UserRole": dev_role, - "ResourceList": "mock-cluster.example.edu", + "ResourceList": ["mock-cluster.example.edu"], })) - return packets # Scenario mix SUCCESS_GENERATORS = [ - (gen_valid_project_create, 3), - (gen_valid_account_create, 3), + (gen_valid_project_create, 2), + (gen_valid_account_create, 2), (gen_valid_user_modify, 1), - (gen_inform_transaction_complete, 2), + (gen_valid_data_account_create, 1), + (gen_valid_data_project_create, 1), + (gen_valid_project_inactivate, 1), + (gen_valid_project_reactivate, 1), + (gen_valid_account_inactivate, 1), + (gen_valid_account_reactivate, 1), + (gen_valid_person_merge, 1), + (gen_inform_transaction_complete, 1), ] FAILURE_GENERATORS = [ (gen_missing_global_id, 2), (gen_missing_grant_number, 1), - (gen_missing_email, 2), - (gen_missing_pi_name, 1), - (gen_invalid_person_modify, 2), + (gen_missing_pi_global_id, 1), (gen_unknown_packet_type, 1), (gen_empty_body, 1), ] def generate_batch(success_count=6, failure_count=4): - """Generate a mixed batch of success and failure packets.""" + """Mixed batch of success and failure packets.""" packets = [] - success_pool = [] for gen, weight in SUCCESS_GENERATORS: success_pool.extend([gen] * weight) - failure_pool = [] for gen, weight in FAILURE_GENERATORS: failure_pool.extend([gen] * weight) - for _ in range(success_count): - gen = random.choice(success_pool) - packets.append(gen()) - + packets.append(random.choice(success_pool)()) for _ in range(failure_count): - gen = random.choice(failure_pool) - packets.append(gen()) - + packets.append(random.choice(failure_pool)()) random.shuffle(packets) return packets -# API endpoints +def generate_all_handlers_once(): + """One packet per handler type. Useful for verifying every handler runs. + + For request_person_merge to actually exercise the merge path (rather than + erroring on missing users), the scenario pre-creates two users via + request_account_create with deterministic UserGlobalIDs before emitting + the merge. Same trick for data_account_create / data_project_create: + a prior request_account_create establishes the user that the DN-pass + handler will look up by GlobalID.""" + primary_gid = f"merge-primary-{random.randint(100000, 999999)}" + secondary_gid = f"merge-secondary-{random.randint(100000, 999999)}" + data_user_gid = f"data-user-{random.randint(100000, 999999)}" + data_pi_gid = f"data-pi-{random.randint(100000, 999999)}" + + def account_create_with_gid(gid): + return make_packet("request_account_create", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "GrantNumber": f"TST{random.randint(100000, 999999)}", + "UserPersonID": f"person-{uuid.uuid4().hex[:8]}", + "UserGlobalID": gid, + "UserFirstName": "Test", + "UserLastName": "User", + "UserEmail": f"{gid}@example.edu", + "UserOrganization": "Mock Institute", + "UserOrgCode": "MI", + "NsfStatusCode": "AC", + "UserDnList": [], + "ResourceList": ["mock-cluster.example.edu"], + }) + + return [ + # Set up: create the users that downstream handlers need. + account_create_with_gid(primary_gid), + account_create_with_gid(secondary_gid), + account_create_with_gid(data_user_gid), + account_create_with_gid(data_pi_gid), + # One packet per handler type: + gen_valid_project_create(), + gen_valid_account_create(), + gen_valid_user_modify(), + make_packet("data_account_create", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "GlobalID": data_user_gid, + "DnList": [f"/C=US/O=Mock/CN=User {data_user_gid}"], + }), + make_packet("data_project_create", { + "ProjectID": f"PRJ-MOCK{random.randint(1000, 9999)}", + "PersonID": f"person-{uuid.uuid4().hex[:8]}", + "GlobalID": data_pi_gid, + "DnList": [f"/C=US/O=Mock/CN=PI {data_pi_gid}"], + }), + gen_valid_project_inactivate(), + gen_valid_project_reactivate(), + gen_valid_account_inactivate(), + gen_valid_account_reactivate(), + make_packet("request_person_merge", { + "KeepPersonID": f"person-keep-{uuid.uuid4().hex[:8]}", + "DeletePersonID": f"person-delete-{uuid.uuid4().hex[:8]}", + "PrimaryGlobalID": primary_gid, + "SecondaryGlobalID": secondary_gid, + "MergeReason": "all_handlers test scenario", + }), + gen_inform_transaction_complete(), + ] + + +# ----- API endpoints ----- @app.route("/packets/<site>", methods=["GET"]) def get_packets(site): - """Return pending packets (mimics AMIE API poll). - Returns a JSON array at the top level, matching the format - that AmieClient.parsePacketsFromResponse() expects.""" if not pending_packets: return jsonify([]) - batch = list(pending_packets) pending_packets.clear() stats["fetched"] += len(batch) - app.logger.info(f"Serving {len(batch)} packets to site {site}") return jsonify(batch) @app.route("/packets/<site>/<int:packet_rec_id>/reply", methods=["POST"]) def reply_to_packet(site, packet_rec_id): - """Accept a reply from the service.""" replied_packets[packet_rec_id] = request.get_json(silent=True) stats["replied"] += 1 return jsonify({"message": "Reply accepted"}), 200 @@ -353,7 +458,6 @@ def reply_to_packet(site, packet_rec_id): @app.route("/test/<site>/reset", methods=["POST"]) def reset(site): - """Reset all state.""" pending_packets.clear() replied_packets.clear() stats["created"] = 0 @@ -365,9 +469,7 @@ def reset(site): @app.route("/test/<site>/scenarios", methods=["POST"]) def create_scenario(site): - """Generate a batch of mixed success/failure scenarios.""" scenario_type = request.args.get("type", "mixed") - if scenario_type == "mixed": packets = generate_batch(success_count=6, failure_count=4) elif scenario_type == "failures_only": @@ -376,42 +478,38 @@ def create_scenario(site): packets = generate_batch(success_count=8, failure_count=0) elif scenario_type == "heavy": packets = generate_batch(success_count=15, failure_count=10) + elif scenario_type == "all_handlers": + packets = generate_all_handlers_once() elif scenario_type == "dev_email": packets = gen_dev_email_scenario() else: packets = generate_batch(success_count=3, failure_count=2) - pending_packets.extend(packets) stats["created"] += len(packets) - app.logger.info(f"Created {len(packets)} packets ({scenario_type}) for site {site}") return jsonify({"message": "Test scenario initiated", "result": None}) @app.route("/stats", methods=["GET"]) def get_stats(): - """Stats endpoint for debugging.""" - return jsonify({ - "pending": len(pending_packets), - "replied": len(replied_packets), - "stats": stats, - }) + return jsonify({"pending": len(pending_packets), "replied": len(replied_packets), "stats": stats}) if __name__ == "__main__": print("Mock AMIE Server starting on http://localhost:8180") - print("Point your service at: access.amie.base-url=http://localhost:8180") - print("") + print("Point your connector at: AMIE_BASE_URL=http://localhost:8180") + print() print("Scenario types:") print(" POST /test/{site}/scenarios?type=mixed — 6 success + 4 failure") print(" POST /test/{site}/scenarios?type=failures_only — 8 failures") print(" POST /test/{site}/scenarios?type=success_only — 8 successes") print(" POST /test/{site}/scenarios?type=heavy — 15 success + 10 failure") - print(" POST /test/{site}/scenarios?type=dev_email — scripted set placing DEV_EMAIL across projects") - print("") + print(" POST /test/{site}/scenarios?type=all_handlers — one packet per handler type") + print(" POST /test/{site}/scenarios?type=dev_email — scripted dev_email scenario") + print() if DEV_EMAIL: print(f"DEV_EMAIL injection enabled: {DEV_EMAIL}") else: - print("DEV_EMAIL is unset — set it (e.g. [email protected]) to use the dev_email scenario") - print("") + print("DEV_EMAIL unset; dev_email scenario will be empty") + print() app.run(host="0.0.0.0", port=8180, debug=False)
