diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/blocked-profile.json b/apps/stalker-mock-server/fixtures/replay/authentication/blocked-profile.json new file mode 100644 index 000000000..d1e5bad5a --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/authentication/blocked-profile.json @@ -0,0 +1,236 @@ +{ + "description": "Synthetic blocked profile terminates without credential retry.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "blocked-profile", + "method": "GET", + "operation": "blocked-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "block_msg": "Synthetic blocked profile", + "status": 1 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "blocked-profile", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "authentication-blocked-profile", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/noncanonical-do-auth.json b/apps/stalker-mock-server/fixtures/replay/authentication/noncanonical-do-auth.json new file mode 100644 index 000000000..2cb4318a4 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/authentication/noncanonical-do-auth.json @@ -0,0 +1,323 @@ +{ + "description": "Synthetic noncanonical do auth success still permits the second profile.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-first", + "method": "GET", + "operation": "profile-first", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 2 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "do-auth-noncanonical", + "method": "GET", + "operation": "do-auth-noncanonical", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username" + }, + "password": { + "kind": "ref", + "symbol": "password" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": 1 + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "noncanonical-do-auth", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "authentication-noncanonical-do-auth", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/saved-rejected-fresh.json b/apps/stalker-mock-server/fixtures/replay/authentication/saved-rejected-fresh.json new file mode 100644 index 000000000..c91eae1d9 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/authentication/saved-rejected-fresh.json @@ -0,0 +1,488 @@ +{ + "description": "Synthetic rejected saved credentials yield a fresh challenge whose credentials can complete profile step two.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-first", + "method": "GET", + "operation": "profile-first", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 2 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "saved-do-auth", + "method": "GET", + "operation": "saved-do-auth", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "saved-username" + }, + "password": { + "kind": "ref", + "symbol": "saved-password" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": false + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "fresh-do-auth", + "method": "GET", + "operation": "fresh-do-auth", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "fresh-username" + }, + "password": { + "kind": "ref", + "symbol": "fresh-password" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": true + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-second", + "method": "GET", + "operation": "profile-second", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "1", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "saved-rejected-fresh", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "authentication-saved-rejected-fresh", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "saved-username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "saved-password", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "fresh-username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "fresh-password", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json b/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json new file mode 100644 index 000000000..a86ab284d --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/authentication/status2-second-step.json @@ -0,0 +1,412 @@ +{ + "description": "Synthetic status two profile requires do auth before the second profile.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "random": { + "kind": "generate", + "symbol": "challenge", + "valueKind": "random" + }, + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-first", + "method": "GET", + "operation": "profile-first", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "random": { + "kind": "ref", + "symbol": "challenge" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 2 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "do-auth", + "method": "GET", + "operation": "do-auth", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username" + }, + "password": { + "kind": "ref", + "symbol": "password" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": true + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-second", + "method": "GET", + "operation": "profile-second", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "1", + "random": { + "kind": "ref", + "symbol": "challenge" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "conditional-second-step", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "authentication-status2-second-step", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/authentication/three-attempt-limit.json b/apps/stalker-mock-server/fixtures/replay/authentication/three-attempt-limit.json new file mode 100644 index 000000000..a0a6c26e0 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/authentication/three-attempt-limit.json @@ -0,0 +1,499 @@ +{ + "description": "Synthetic credentials are rejected exactly three times before the connection attempt ends.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-probe", + "method": "GET", + "operation": "anonymous-probe", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "handshake", + "method": "GET", + "operation": "handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "profile-status-two", + "method": "GET", + "operation": "profile-status-two", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": 2 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "credential-attempt-one", + "method": "GET", + "operation": "credential-attempt-one", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username-one" + }, + "password": { + "kind": "ref", + "symbol": "password-one" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": false + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "credential-attempt-two", + "method": "GET", + "operation": "credential-attempt-two", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username-two" + }, + "password": { + "kind": "ref", + "symbol": "password-two" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": false + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "credential-attempt-three", + "method": "GET", + "operation": "credential-attempt-three", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "do_auth", + "login": { + "kind": "ref", + "symbol": "username-three" + }, + "password": { + "kind": "ref", + "symbol": "password-three" + }, + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": false + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "three-attempt-limit", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "authentication-three-attempt-limit", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "username-one", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password-one", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "username-two", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password-two", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "username-three", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password-three", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/classifiers/access-denied-200.json b/apps/stalker-mock-server/fixtures/replay/classifiers/access-denied-200.json new file mode 100644 index 000000000..ad1757e43 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/classifiers/access-denied-200.json @@ -0,0 +1,81 @@ +{ + "description": "Synthetic HTTP 200 body denial does not imply token invalidation.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "body-denial", + "method": "GET", + "operation": "body-denial", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "get_profile", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "error": "Access denied" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "body-denial", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "classifier-access-denied-200", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/classifiers/oversized-response.json b/apps/stalker-mock-server/fixtures/replay/classifiers/oversized-response.json new file mode 100644 index 000000000..2daa3eb0e --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/classifiers/oversized-response.json @@ -0,0 +1,78 @@ +{ + "description": "Synthetic response crosses the session response-size classifier bound.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "oversized-response", + "method": "GET", + "operation": "oversized-response", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "get_profile", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "byte": 97, + "byteLength": 4194305, + "kind": "generated" + }, + "headers": { + "content-type": [ + "application/octet-stream" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "oversized-response", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "classifier-oversized-response", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/classifiers/rate-limit-429.json b/apps/stalker-mock-server/fixtures/replay/classifiers/rate-limit-429.json new file mode 100644 index 000000000..896f1a0cb --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/classifiers/rate-limit-429.json @@ -0,0 +1,82 @@ +{ + "description": "Synthetic HTTP 429 response preserves bounded retry evidence.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "rate-limit", + "method": "GET", + "operation": "rate-limit", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "get_profile", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "error": "synthetic-rate-limit" + } + }, + "headers": { + "content-type": [ + "application/json" + ], + "retry-after": [ + "3" + ] + }, + "status": 429 + } + } + ], + "mode": "ordered", + "name": "rate-limit", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "classifier-rate-limit-429", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/classifiers/service-unavailable-503.json b/apps/stalker-mock-server/fixtures/replay/classifiers/service-unavailable-503.json new file mode 100644 index 000000000..26721805b --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/classifiers/service-unavailable-503.json @@ -0,0 +1,79 @@ +{ + "description": "Synthetic HTTP 503 response is a retryable service failure.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "service-unavailable", + "method": "GET", + "operation": "service-unavailable", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "get_profile", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "error": "synthetic-unavailable" + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 503 + } + } + ], + "mode": "ordered", + "name": "service-unavailable", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "classifier-service-unavailable-503", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/classifiers/waf-403.json b/apps/stalker-mock-server/fixtures/replay/classifiers/waf-403.json new file mode 100644 index 000000000..b570f7627 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/classifiers/waf-403.json @@ -0,0 +1,77 @@ +{ + "description": "Synthetic HTTP 403 WAF response is distinct from token rejection.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "waf-response", + "method": "GET", + "operation": "waf-response", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "get_profile", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "text", + "value": "Synthetic Web Application Firewall challenge" + }, + "headers": { + "content-type": [ + "text/html" + ] + }, + "status": 403 + } + } + ], + "mode": "ordered", + "name": "waf-response", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "classifier-waf-403", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/cookies/managed-shadow.json b/apps/stalker-mock-server/fixtures/replay/cookies/managed-shadow.json new file mode 100644 index 000000000..828037b95 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/cookies/managed-shadow.json @@ -0,0 +1,162 @@ +{ + "description": "Synthetic server cookie cannot shadow the manager-owned MAC through a narrower path.", + "entry": { + "origin": "portal", + "path": "/shadow" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/managed" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "shadow-cookie", + "method": "GET", + "operation": "shadow-cookie", + "origin": "portal", + "path": "/shadow", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "managed-mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": "synthetic-managed" + } + } + }, + "headers": { + "set-cookie": [ + { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "mac=" + }, + { + "kind": "ref", + "symbol": "shadow-mac" + }, + { + "kind": "literal", + "value": "; Path=/shadow" + } + ] + } + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "shadow-cookie", + "nextState": "managed-cookie", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "managed-cookie", + "method": "GET", + "operation": "managed-cookie", + "origin": "portal", + "path": "/managed", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "managed-mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + } + ], + "mode": "ordered", + "name": "managed-cookie", + "nextState": "complete", + "state": "managed-cookie" + } + ], + "scenarioId": "cookies-managed-shadow", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "managed-mac", + "valueKind": "mac" + }, + { + "kind": "generate", + "symbol": "shadow-mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/cookies/session-isolation.json b/apps/stalker-mock-server/fixtures/replay/cookies/session-isolation.json new file mode 100644 index 000000000..5e60d181a --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/cookies/session-isolation.json @@ -0,0 +1,216 @@ +{ + "description": "Synthetic cookie remains isolated to its issuing origin.", + "entry": { + "origin": "portal", + "path": "/issue" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/return" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "auth": {}, + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "portal-cookie", + "method": "GET", + "operation": "portal-cookie", + "origin": "portal", + "path": "/issue", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "auth", + "path": "/check" + } + ], + "set-cookie": [ + { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "session=" + }, + { + "kind": "ref", + "symbol": "portal-session" + }, + { + "kind": "literal", + "value": "; Path=/; HttpOnly" + } + ] + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "portal-cookie", + "nextState": "cross-origin", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "cross-origin", + "method": "GET", + "operation": "cross-origin", + "origin": "auth", + "path": "/check", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "portal", + "path": "/return" + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "cross-origin", + "nextState": "return", + "state": "cross-origin" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "return", + "method": "GET", + "operation": "return", + "origin": "portal", + "path": "/return", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": { + "session": { + "kind": "ref", + "symbol": "portal-session" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + } + ], + "mode": "ordered", + "name": "return", + "nextState": "complete", + "state": "return" + } + ], + "scenarioId": "cookies-session-isolation", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "portal-session", + "valueKind": "cookie" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/redirects/auth-query.json b/apps/stalker-mock-server/fixtures/replay/redirects/auth-query.json new file mode 100644 index 000000000..6e8a20fe1 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/redirects/auth-query.json @@ -0,0 +1,104 @@ +{ + "description": "Synthetic redirect uses typed generated authentication query fields.", + "entry": { + "origin": "portal", + "path": "/entry" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/entry" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "auth": {}, + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "typed-auth-query", + "method": "GET", + "operation": "typed-auth-query", + "origin": "portal", + "path": "/entry", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "username", + "password", + "token" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "auth", + "path": "/login", + "query": { + "auth": { + "kind": "generate", + "symbol": "redirect-auth", + "valueKind": "token" + }, + "login": { + "kind": "ref", + "symbol": "username" + } + } + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "typed-auth-query", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "redirect-auth-query", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "username", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/redirects/identity-pause.json b/apps/stalker-mock-server/fixtures/replay/redirects/identity-pause.json new file mode 100644 index 000000000..6aabc71e0 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/redirects/identity-pause.json @@ -0,0 +1,219 @@ +{ + "description": "Synthetic redirect pauses identity until landing approval completes.", + "entry": { + "origin": "portal", + "path": "/entry" + }, + "expectedEndpoint": { + "origin": "landing", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "landing": {}, + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "anonymous-redirect", + "method": "GET", + "operation": "anonymous-redirect", + "origin": "portal", + "path": "/entry", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "mac", + "token" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "landing", + "path": "/approve" + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "anonymous-redirect", + "nextState": "paused", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "approval", + "method": "GET", + "operation": "approval", + "origin": "landing", + "path": "/approve", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "mac", + "token" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + } + ], + "mode": "ordered", + "name": "approval", + "nextState": "identity-ready", + "state": "paused" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "identity-request", + "method": "GET", + "operation": "identity-request", + "origin": "landing", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "token" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": "synthetic-ready" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "identity-request", + "nextState": "complete", + "state": "identity-ready" + } + ], + "scenarioId": "redirect-identity-pause", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/redirects/landing-approval.json b/apps/stalker-mock-server/fixtures/replay/redirects/landing-approval.json new file mode 100644 index 000000000..fd1f16b2b --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/redirects/landing-approval.json @@ -0,0 +1,154 @@ +{ + "description": "Synthetic cross-origin landing requires explicit approval before use.", + "entry": { + "origin": "portal", + "path": "/c/" + }, + "expectedEndpoint": { + "origin": "landing", + "path": "/approve" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "landing": {}, + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "landing-redirect", + "method": "GET", + "operation": "landing-redirect", + "origin": "portal", + "path": "/c/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "mac", + "token", + "username", + "password" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "landing", + "path": "/approve" + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "landing-redirect", + "nextState": "approval", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "approved-landing", + "method": "GET", + "operation": "approved-landing", + "origin": "landing", + "path": "/approve", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "mac", + "token", + "username", + "password" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "approval": "synthetic-required" + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "approved-landing", + "nextState": "complete", + "state": "approval" + } + ], + "scenarioId": "redirect-landing-approval", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/redirects/malicious-user-info.json b/apps/stalker-mock-server/fixtures/replay/redirects/malicious-user-info.json new file mode 100644 index 000000000..e1e6aa6dd --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/redirects/malicious-user-info.json @@ -0,0 +1,111 @@ +{ + "description": "Synthetic redirect carries typed user-info for fail-closed review.", + "entry": { + "origin": "portal", + "path": "/entry" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/entry" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "auth": {}, + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "credential-bearing-redirect", + "method": "GET", + "operation": "credential-bearing-redirect", + "origin": "portal", + "path": "/entry", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "username", + "password", + "token" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + { + "kind": "origin-url", + "origin": "auth", + "path": "/login", + "query": { + "continue": "synthetic" + }, + "userInfo": { + "password": { + "kind": "ref", + "symbol": "password" + }, + "username": { + "kind": "ref", + "symbol": "username" + } + } + } + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "credential-bearing-redirect", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "redirect-malicious-user-info", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "username", + "valueKind": "credential" + }, + { + "kind": "generate", + "symbol": "password", + "valueKind": "credential" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/redirects/repeated-loop.json b/apps/stalker-mock-server/fixtures/replay/redirects/repeated-loop.json new file mode 100644 index 000000000..48743ba7d --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/redirects/repeated-loop.json @@ -0,0 +1,73 @@ +{ + "description": "Synthetic repeated redirect reaches the bounded loop ceiling.", + "entry": { + "origin": "portal", + "path": "/loop" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/loop" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 3, + "min": 3 + }, + "id": "bounded-loop", + "method": "GET", + "operation": "bounded-loop", + "origin": "portal", + "path": "/loop", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": { + "location": [ + "/loop" + ] + }, + "status": 302 + } + } + ], + "mode": "ordered", + "name": "bounded-loop", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "redirect-repeated-loop", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/refresh/concurrent-rejections.json b/apps/stalker-mock-server/fixtures/replay/refresh/concurrent-rejections.json new file mode 100644 index 000000000..9d572f50e --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/refresh/concurrent-rejections.json @@ -0,0 +1,654 @@ +{ + "description": "Synthetic concurrent token rejections share one refresh and retry both operations exactly once.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "bootstrap", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "initial-handshake", + "method": "GET", + "operation": "initial-handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "old-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "initial-profile", + "method": "GET", + "operation": "initial-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "old-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "initial-session", + "nextState": "start", + "state": "bootstrap" + }, + { + "barrier": { + "name": "both-rejected", + "releaseWhenMatched": 2 + }, + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "first-rejection", + "method": "GET", + "operation": "first-rejection", + "origin": "portal", + "path": "/one", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "old-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_ordered_list", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "error": "Authorization failed" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 401 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "second-rejection", + "method": "GET", + "operation": "second-rejection", + "origin": "portal", + "path": "/two", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "old-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_epg_info", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "error": "Authorization failed" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 401 + } + } + ], + "mode": "unordered", + "name": "concurrent-rejections", + "nextState": "refresh", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "refresh-handshake", + "method": "GET", + "operation": "refresh-handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "new-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "refresh-profile", + "method": "GET", + "operation": "refresh-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "new-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "single-refresh", + "nextState": "retry", + "state": "refresh" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "first-retry", + "method": "GET", + "operation": "first-retry", + "origin": "portal", + "path": "/one", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "new-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_ordered_list", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "data": [] + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "second-retry", + "method": "GET", + "operation": "second-retry", + "origin": "portal", + "path": "/two", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "new-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_epg_info", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "data": [] + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "unordered", + "name": "retried-operations", + "nextState": "complete", + "state": "retry" + } + ], + "scenarioId": "refresh-concurrent-rejections", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/refresh/exhausted-retry.json b/apps/stalker-mock-server/fixtures/replay/refresh/exhausted-retry.json new file mode 100644 index 000000000..b69873e8d --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/refresh/exhausted-retry.json @@ -0,0 +1,500 @@ +{ + "description": "Synthetic token rejection triggers one refresh and one terminally rejected retry without recursion.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "bootstrap", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "initial-handshake", + "method": "GET", + "operation": "initial-handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "old-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "initial-profile", + "method": "GET", + "operation": "initial-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "old-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "initial-session", + "nextState": "start", + "state": "bootstrap" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "initial-rejection", + "method": "GET", + "operation": "initial-rejection", + "origin": "portal", + "path": "/catalog", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "old-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_ordered_list", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "error": "Authorization failed" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 401 + } + } + ], + "mode": "ordered", + "name": "initial-rejection", + "nextState": "refresh", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "refresh-handshake", + "method": "GET", + "operation": "refresh-handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "new-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "refresh-profile", + "method": "GET", + "operation": "refresh-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "new-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "single-refresh", + "nextState": "retry", + "state": "refresh" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "terminal-rejection", + "method": "GET", + "operation": "terminal-rejection", + "origin": "portal", + "path": "/catalog", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "new-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_ordered_list", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "error": "Authorization failed" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 401 + } + } + ], + "mode": "ordered", + "name": "terminal-retry", + "nextState": "complete", + "state": "retry" + } + ], + "scenarioId": "refresh-exhausted-retry", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/resolver/direct-load-jsonp.json b/apps/stalker-mock-server/fixtures/replay/resolver/direct-load-jsonp.json new file mode 100644 index 000000000..20314cabe --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/resolver/direct-load-jsonp.json @@ -0,0 +1,185 @@ +{ + "description": "Synthetic direct load endpoint completes handshake and profile through allowlisted JSONP envelopes.", + "entry": { + "origin": "portal", + "path": "/server/load.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/server/load.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "direct-load-handshake", + "method": "GET", + "operation": "direct-load-handshake", + "origin": "portal", + "path": "/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "callback": "callback", + "kind": "jsonp", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/javascript" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "direct-load-profile", + "method": "GET", + "operation": "direct-load-profile", + "origin": "portal", + "path": "/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "JsHttpRequest": "1-xml", + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "callback": "callback", + "kind": "jsonp", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/javascript" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "direct-load", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "resolver-direct-load-jsonp", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/resolver/direct-portal-statusless.json b/apps/stalker-mock-server/fixtures/replay/resolver/direct-portal-statusless.json new file mode 100644 index 000000000..e7a3ed5e7 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/resolver/direct-portal-statusless.json @@ -0,0 +1,184 @@ +{ + "description": "Synthetic direct portal endpoint completes a handshake and recognized statusless profile.", + "entry": { + "origin": "portal", + "path": "/portal.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "direct-handshake", + "method": "GET", + "operation": "direct-handshake", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "session-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "statusless-profile", + "method": "GET", + "operation": "statusless-profile", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "session-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 7, + "mac": { + "kind": "ref", + "symbol": "mac" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "direct-portal", + "nextState": "complete", + "state": "start" + } + ], + "scenarioId": "resolver-direct-portal-statusless", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/resolver/learned-rediscovery.json b/apps/stalker-mock-server/fixtures/replay/resolver/learned-rediscovery.json new file mode 100644 index 000000000..5df8af790 --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/resolver/learned-rediscovery.json @@ -0,0 +1,135 @@ +{ + "description": "Synthetic stale learned route falls back to safe rediscovery.", + "entry": { + "origin": "portal", + "path": "/learned/load.php" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/portal.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "learned-route", + "method": "GET", + "operation": "learned-route", + "origin": "portal", + "path": "/learned/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "error": "synthetic-missing" + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 404 + } + } + ], + "mode": "ordered", + "name": "learned-route", + "nextState": "rediscover", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "rediscovered-route", + "method": "GET", + "operation": "rediscovered-route", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [], + "exact": {}, + "present": [] + }, + "query": { + "absent": [], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "status": "synthetic-ready" + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "rediscovered-route", + "nextState": "complete", + "state": "rediscover" + } + ], + "scenarioId": "resolver-learned-rediscovery", + "schemaVersion": 1, + "symbols": [], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/fixtures/replay/resolver/root-full-wins.json b/apps/stalker-mock-server/fixtures/replay/resolver/root-full-wins.json new file mode 100644 index 000000000..8d62db9ee --- /dev/null +++ b/apps/stalker-mock-server/fixtures/replay/resolver/root-full-wins.json @@ -0,0 +1,319 @@ +{ + "description": "Synthetic root discovery records an early stateless candidate but selects the later full-session endpoint.", + "entry": { + "origin": "portal", + "path": "/" + }, + "expectedEndpoint": { + "origin": "portal", + "path": "/stalker_portal/server/load.php" + }, + "failOnUnexpectedRequest": true, + "initialState": "start", + "origins": { + "portal": {} + }, + "phases": [ + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "root-probe", + "method": "GET", + "operation": "root-probe", + "origin": "portal", + "path": "/", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "mac", + "session" + ], + "attributes": {}, + "exact": {}, + "present": [] + }, + "headers": { + "absent": [ + "authorization", + "cookie" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "device_id", + "device_id2", + "mac", + "password", + "signature", + "signature2", + "sn", + "token", + "username" + ], + "exact": {}, + "present": [] + } + }, + "response": { + "body": { + "kind": "empty" + }, + "headers": {}, + "status": 204 + } + } + ], + "mode": "ordered", + "name": "root-probe", + "nextState": "stateless-candidate", + "state": "start" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "stateless-capability", + "method": "GET", + "operation": "stateless-capability", + "origin": "portal", + "path": "/portal.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "get_genres", + "type": "itv" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": [ + { + "id": "1", + "title": "Synthetic" + } + ] + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "stateless-candidate", + "nextState": "full-candidate", + "state": "stateless-candidate" + }, + { + "expectations": [ + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "full-handshake", + "method": "GET", + "operation": "full-handshake", + "origin": "portal", + "path": "/stalker_portal/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [ + "authorization" + ], + "exact": {}, + "present": [] + }, + "query": { + "absent": [ + "password", + "token", + "username" + ], + "exact": { + "action": "handshake", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "token": { + "kind": "generate", + "symbol": "full-token", + "valueKind": "token" + } + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + }, + { + "cardinality": { + "max": 1, + "min": 1 + }, + "id": "full-profile", + "method": "GET", + "operation": "full-profile", + "origin": "portal", + "path": "/stalker_portal/server/load.php", + "request": { + "body": { + "kind": "absent" + }, + "cookies": { + "absent": [ + "session" + ], + "attributes": {}, + "exact": { + "mac": { + "kind": "ref", + "symbol": "mac" + } + }, + "present": [] + }, + "headers": { + "absent": [], + "exact": { + "authorization": { + "kind": "parts", + "parts": [ + { + "kind": "literal", + "value": "Bearer " + }, + { + "kind": "ref", + "symbol": "full-token" + } + ] + } + }, + "present": [] + }, + "query": { + "absent": [ + "password", + "username" + ], + "exact": { + "action": "get_profile", + "auth_second_step": "0", + "type": "stb" + }, + "present": [] + } + }, + "response": { + "body": { + "kind": "json", + "value": { + "js": { + "id": 1, + "status": 0 + } + } + }, + "headers": { + "content-type": [ + "application/json" + ] + }, + "status": 200 + } + } + ], + "mode": "ordered", + "name": "full-candidate", + "nextState": "complete", + "state": "full-candidate" + } + ], + "scenarioId": "resolver-root-full-wins", + "schemaVersion": 1, + "symbols": [ + { + "kind": "generate", + "symbol": "mac", + "valueKind": "mac" + } + ], + "terminalState": "complete" +} diff --git a/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts b/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts index 746ebdd9d..2af4ce0c7 100644 --- a/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts +++ b/apps/stalker-mock-server/src/app/replay/replay-fixture-corpus.spec.ts @@ -1,14 +1,69 @@ +/* eslint-disable max-lines -- The committed corpus matrix and its typed deterministic driver form one auditable contract. */ import { readdirSync, readFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { createReplayRun, type ReplayRun } from './replay-run.js'; import { parseReplayFixtureText } from './replay-schema.js'; -import type { ReplayObservedRequest } from './replay.types.js'; +import type { + ReplayExpectation, + ReplayFixtureV1, + ReplayGenerateNode, + ReplayLiteralPart, + ReplayObservedCookie, + ReplayObservedRequest, + ReplayPartsNode, + ReplayRefNode, + ReplayRenderedResponse, + ReplayResponseHeaderValue, + ReplayTemplateString, + ReplayTemplateValue, +} from './replay.types.js'; const FIXTURE_ROOT = resolve( process.cwd(), 'apps/stalker-mock-server/fixtures/replay' ); +const EXPECTED_SCENARIOS = [ + 'authentication-blocked-profile', + 'authentication-noncanonical-do-auth', + 'authentication-saved-rejected-fresh', + 'authentication-status2-second-step', + 'authentication-three-attempt-limit', + 'classifier-access-denied-200', + 'classifier-oversized-response', + 'classifier-rate-limit-429', + 'classifier-service-unavailable-503', + 'classifier-waf-403', + 'cookies-managed-shadow', + 'cookies-session-isolation', + 'redirect-auth-query', + 'redirect-identity-pause', + 'redirect-landing-approval', + 'redirect-malicious-user-info', + 'redirect-repeated-loop', + 'refresh-concurrent-rejections', + 'refresh-exhausted-retry', + 'resolver-direct-load-jsonp', + 'resolver-direct-portal-statusless', + 'resolver-learned-rediscovery', + 'resolver-root-full-wins', + 'resolver-root-landing', +] as const; + +const RESERVED_EARLY_QUERY_FIELDS = [ + 'device_id', + 'device_id2', + 'mac', + 'password', + 'signature', + 'signature2', + 'sn', + 'token', + 'username', +] as const; +const RESERVED_EARLY_HEADERS = ['authorization', 'cookie'] as const; +const RESERVED_EARLY_COOKIES = ['mac', 'session'] as const; + function collectFixturePaths(directory: string): string[] { return readdirSync(directory, { withFileTypes: true }) .sort((left, right) => compareCodeUnits(left.name, right.name)) @@ -24,48 +79,554 @@ function collectFixturePaths(directory: string): string[] { }); } -function emptyGet(path: string): ReplayObservedRequest { +function resolveString( + value: ReplayTemplateString, + bindings: ReadonlyMap +): string { + if (typeof value === 'string') { + return value; + } + if (value.kind === 'ref') { + const resolved = bindings.get(value.symbol); + if (resolved === undefined) { + throw new Error(`Fixture driver symbol missing: ${value.symbol}.`); + } + return resolved; + } + if (value.kind === 'parts') { + return value.parts + .map((part) => + part.kind === 'literal' + ? part.value + : resolveString(part, bindings) + ) + .join(''); + } + throw new Error( + `Fixture request cannot generate symbol: ${value.symbol}.` + ); +} + +function templateNodeKind(value: object): unknown { + return (value as { kind?: unknown }).kind; +} + +function resolveValue( + value: ReplayTemplateValue, + bindings: ReadonlyMap +): unknown { + if ( + value === null || + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' + ) { + return value; + } + if (Array.isArray(value)) { + return value.map((item) => resolveValue(item, bindings)); + } + const kind = templateNodeKind(value); + if (kind === 'ref' || kind === 'parts' || kind === 'generate') { + return resolveString(value as ReplayTemplateString, bindings); + } + return Object.fromEntries( + Object.entries(value).map(([key, child]) => [ + key, + resolveValue(child, bindings), + ]) + ); +} + +function resolveRepeatedFields( + exact: ReplayExpectation['request']['query']['exact'], + present: readonly string[], + bindings: ReadonlyMap +): Record { + const fields = Object.create(null) as Record; + for (const [name, value] of Object.entries(exact)) { + const values = Array.isArray(value) ? value : [value]; + fields[name] = values.map((item) => + resolveString(item, bindings) + ); + } + for (const name of present) { + fields[name] ??= ['synthetic-present']; + } + return fields; +} + +function observedRequest( + expectation: ReplayExpectation, + bindings: ReadonlyMap +): ReplayObservedRequest { + const cookies = Object.create(null) as Record< + string, + ReplayObservedCookie + >; + for (const [name, value] of Object.entries( + expectation.request.cookies.exact + )) { + const attributes = expectation.request.cookies.attributes[name]; + cookies[name] = { + value: resolveString(value, bindings), + ...(attributes === undefined + ? {} + : { + attributes: Object.fromEntries( + Object.entries(attributes).map( + ([attribute, attributeValue]) => [ + attribute, + typeof attributeValue === 'object' || + typeof attributeValue === 'string' + ? resolveString( + attributeValue as ReplayTemplateString, + bindings + ) + : attributeValue, + ] + ) + ), + }), + }; + } + for (const name of expectation.request.cookies.present) { + cookies[name] ??= { value: 'synthetic-present' }; + } + + const matcher = expectation.request.body; + const body: ReplayObservedRequest['body'] = + matcher.kind === 'absent' + ? { kind: 'absent' } + : matcher.kind === 'text' + ? { + kind: 'text', + value: resolveString(matcher.exact, bindings), + } + : matcher.kind === 'form' + ? { + kind: 'form', + value: Object.fromEntries([ + ...Object.entries(matcher.exact).map( + ([name, value]) => [ + name, + resolveString(value, bindings), + ] + ), + ...matcher.present + .filter( + (name) => + !Object.prototype.hasOwnProperty.call( + matcher.exact, + name + ) + ) + .map((name) => [ + name, + 'synthetic-present', + ]), + ]), + } + : { + kind: 'json', + value: Object.fromEntries([ + ...Object.entries(matcher.exact).map( + ([name, value]) => [ + name, + resolveValue(value, bindings), + ] + ), + ...matcher.present + .filter( + (name) => + !Object.prototype.hasOwnProperty.call( + matcher.exact, + name + ) + ) + .map((name) => [ + name, + 'synthetic-present', + ]), + ]) as ReplayObservedRequest['body'] extends { + kind: 'json'; + value: infer Value; + } + ? Value + : never, + }; + return { - origin: 'portal', - method: 'GET', - path, - query: {}, - headers: {}, - cookies: {}, - body: { kind: 'absent' }, + origin: expectation.origin, + method: expectation.method, + path: expectation.path, + query: resolveRepeatedFields( + expectation.request.query.exact, + expectation.request.query.present, + bindings + ), + headers: resolveRepeatedFields( + expectation.request.headers.exact, + expectation.request.headers.present, + bindings + ), + cookies, + body, }; } -const FIXTURE_DRIVERS: Readonly< - Record Promise> -> = { - 'resolver-root-landing': async (run) => { - await run.request(emptyGet('/c/')); - await run.request(emptyGet('/portal.php')); - }, -}; +function captureTemplateValue( + template: ReplayTemplateValue, + rendered: unknown, + bindings: Map +): void { + if ( + template === null || + typeof template === 'boolean' || + typeof template === 'number' || + typeof template === 'string' + ) { + return; + } + if (Array.isArray(template)) { + if (!Array.isArray(rendered)) { + return; + } + template.forEach((item, index) => + captureTemplateValue(item, rendered[index], bindings) + ); + return; + } + const kind = templateNodeKind(template); + if (kind === 'generate') { + const generated = template as ReplayGenerateNode; + if (typeof rendered !== 'string') { + throw new Error( + `Generated fixture symbol was not rendered as a string: ${generated.symbol}.` + ); + } + bindings.set(generated.symbol, rendered); + return; + } + if (kind === 'ref') { + if (typeof rendered === 'string') { + captureRefBinding( + template as ReplayRefNode, + rendered, + bindings + ); + } + return; + } + if (kind === 'parts') { + if (typeof rendered === 'string') { + capturePartsBindings( + template as ReplayPartsNode, + rendered, + bindings + ); + } + return; + } + if (rendered === null || typeof rendered !== 'object') { + return; + } + for (const [key, child] of Object.entries(template)) { + captureTemplateValue( + child, + (rendered as Record)[key], + bindings + ); + } +} + +function captureRefBinding( + template: ReplayRefNode, + rendered: string, + bindings: Map +): void { + const existing = bindings.get(template.symbol); + if (existing !== undefined && existing !== rendered) { + throw new Error( + `Fixture driver symbol changed: ${template.symbol}.` + ); + } + bindings.set(template.symbol, rendered); +} + +function capturePartsBindings( + template: ReplayPartsNode, + rendered: string, + bindings: Map +): void { + let cursor = 0; + template.parts.forEach((part, index) => { + if (part.kind === 'literal') { + if (!rendered.startsWith(part.value, cursor)) { + throw new Error( + 'Fixture driver could not match rendered parts.' + ); + } + cursor += part.value.length; + return; + } + + const existing = bindings.get(part.symbol); + if (existing !== undefined) { + if (!rendered.startsWith(existing, cursor)) { + throw new Error( + `Fixture driver symbol changed: ${part.symbol}.` + ); + } + cursor += existing.length; + return; + } + + const nextLiteral = template.parts + .slice(index + 1) + .find( + ( + candidate + ): candidate is ReplayLiteralPart => + candidate.kind === 'literal' && + candidate.value.length > 0 + ); + const boundary = + nextLiteral === undefined + ? rendered.length + : rendered.indexOf(nextLiteral.value, cursor); + if (boundary < cursor) { + throw new Error( + `Fixture driver could not delimit symbol: ${part.symbol}.` + ); + } + bindings.set(part.symbol, rendered.slice(cursor, boundary)); + cursor = boundary; + }); + if (cursor !== rendered.length) { + throw new Error('Fixture driver left unmatched rendered parts.'); + } +} + +function captureHeaderValue( + template: ReplayResponseHeaderValue, + rendered: string, + bindings: Map +): void { + if (typeof template === 'string') { + return; + } + if (template.kind === 'generate') { + bindings.set(template.symbol, rendered); + return; + } + if (template.kind === 'ref') { + captureRefBinding(template, rendered, bindings); + return; + } + if (template.kind === 'parts') { + capturePartsBindings(template, rendered, bindings); + return; + } + if (template.kind !== 'origin-url') { + return; + } + const url = new URL(rendered); + if ( + template.userInfo?.username !== undefined && + typeof template.userInfo.username !== 'string' && + template.userInfo.username.kind === 'generate' + ) { + bindings.set( + template.userInfo.username.symbol, + decodeURIComponent(url.username) + ); + } + if ( + template.userInfo?.password !== undefined && + typeof template.userInfo.password !== 'string' && + template.userInfo.password.kind === 'generate' + ) { + bindings.set( + template.userInfo.password.symbol, + decodeURIComponent(url.password) + ); + } + for (const [name, queryValue] of Object.entries( + template.query ?? {} + )) { + const templates = Array.isArray(queryValue) + ? queryValue + : [queryValue]; + const values = url.searchParams.getAll(name); + templates.forEach((item, index) => { + if ( + typeof item !== 'string' && + item.kind === 'generate' && + values[index] !== undefined + ) { + bindings.set(item.symbol, values[index]); + } + }); + } +} + +function captureResponseBindings( + expectation: ReplayExpectation, + response: ReplayRenderedResponse, + bindings: Map +): void { + for (const [name, templates] of Object.entries( + expectation.response.headers + )) { + const renderedValues = response.headers[name] ?? []; + templates.forEach((template, index) => { + const rendered = renderedValues[index]; + if (rendered !== undefined) { + captureHeaderValue(template, rendered, bindings); + } + }); + } + const body = expectation.response.body; + if (body.kind === 'json') { + captureTemplateValue( + body.value, + JSON.parse(response.body.toString('utf8')), + bindings + ); + } else if (body.kind === 'jsonp') { + const text = response.body.toString('utf8'); + const prefix = `${body.callback}(`; + captureTemplateValue( + body.value, + JSON.parse(text.slice(prefix.length, -2)), + bindings + ); + } else if ( + body.kind === 'text' && + typeof body.value !== 'string' && + body.value.kind === 'generate' + ) { + bindings.set(body.value.symbol, response.body.toString('utf8')); + } +} + +async function driveFixture( + run: ReplayRun, + fixture: ReplayFixtureV1 +): Promise { + const bindings = new Map(Object.entries(run.getGeneratedInputs())); + for (const phase of fixture.phases) { + if (phase.barrier !== undefined) { + const requests = phase.expectations.flatMap( + (expectation) => + Array.from( + { length: expectation.cardinality.min }, + () => ({ + expectation, + request: observedRequest( + expectation, + bindings + ), + }) + ) + ); + const responses = await Promise.all( + requests.map(({ request }) => run.request(request)) + ); + responses.forEach((response, index) => { + const request = requests[index]; + if (request === undefined) { + throw new Error( + 'Fixture driver lost a barrier request.' + ); + } + captureResponseBindings( + request.expectation, + response, + bindings + ); + }); + continue; + } + for (const expectation of phase.expectations) { + for ( + let matchCount = 0; + matchCount < expectation.cardinality.min; + matchCount += 1 + ) { + const response = await run.request( + observedRequest(expectation, bindings) + ); + captureResponseBindings( + expectation, + response, + bindings + ); + } + } + } +} + +function assertAnonymousRequestHasNoReservedSecrets( + fixture: ReplayFixtureV1 +): void { + const anonymous = fixture.phases[0]?.expectations[0]; + if (anonymous === undefined) { + throw new Error( + `Authentication fixture lacks an anonymous request: ${fixture.scenarioId}.` + ); + } + expect(anonymous.operation).toBe('anonymous-probe'); + expect(anonymous.request.query.absent).toEqual( + expect.arrayContaining([...RESERVED_EARLY_QUERY_FIELDS]) + ); + expect(anonymous.request.headers.absent).toEqual( + expect.arrayContaining([...RESERVED_EARLY_HEADERS]) + ); + expect(anonymous.request.cookies.absent).toEqual( + expect.arrayContaining([...RESERVED_EARLY_COOKIES]) + ); +} describe('committed replay fixture corpus', () => { - it('drives every fixture through exact terminal/cardinality evidence', async () => { - const fixturePaths = collectFixturePaths(FIXTURE_ROOT); - expect(fixturePaths.length).toBeGreaterThan(0); + it('contains the complete Stage-1 scenario matrix', () => { + const scenarioIds = collectFixturePaths(FIXTURE_ROOT) + .map((fixturePath) => + parseReplayFixtureText( + readFileSync(fixturePath, 'utf8') + ) + ) + .map((fixture) => fixture.scenarioId) + .sort(compareCodeUnits); - for (const fixturePath of fixturePaths) { + expect(scenarioIds).toEqual([...EXPECTED_SCENARIOS]); + }); + + it('drives every fixture through exact terminal/cardinality evidence', async () => { + for (const fixturePath of collectFixturePaths(FIXTURE_ROOT)) { const fixture = parseReplayFixtureText( readFileSync(fixturePath, 'utf8') ); - const driver = FIXTURE_DRIVERS[fixture.scenarioId]; - if (driver === undefined) { - throw new Error( - `Replay fixture driver missing: ${fixture.scenarioId}.` - ); + if ( + fixturePath.startsWith( + join(FIXTURE_ROOT, 'authentication') + ) + ) { + assertAnonymousRequestHasNoReservedSecrets(fixture); } const run = createReplayRun(fixture, { runId: `fixture-${fixture.scenarioId}`, + originUrls: Object.fromEntries( + Object.keys(fixture.origins).map((origin) => [ + origin, + `http://${origin}.test/__fixture/${fixture.scenarioId}`, + ]) + ), }); try { - await driver(run); + await driveFixture(run, fixture); expect({ scenarioId: fixture.scenarioId, result: run.finalize(),