mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
test(stalker): expand compatibility replay matrix
This commit is contained in:
1 parent
88f7237874
commit
830405f9d7
8 files changed
+1356
-1
No files matched your search
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Synthetic ambiguous HTTP 403 is incompatible and neither token-rejection nor portal-protection 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": "ambiguous-forbidden",
|
||||
"method": "GET",
|
||||
"operation": "ambiguous-forbidden",
|
||||
"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": "Forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "ambiguous-forbidden",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-ambiguous-403",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Synthetic canonical HTTP 200 authorization failure is explicit token-rejection 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": "authorization-failed",
|
||||
"method": "GET",
|
||||
"operation": "authorization-failed",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_ordered_list",
|
||||
"type": "vod"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"error": "Authorization failed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "authorization-failed",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-authorization-failed-200",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Synthetic canonical HTTP 403 authorization failure is token-rejection evidence rather than an ambiguous forbidden response.",
|
||||
"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": "authorization-failed-forbidden",
|
||||
"method": "GET",
|
||||
"operation": "authorization-failed-forbidden",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_ordered_list",
|
||||
"type": "vod"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"error": "Authorization failed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "authorization-failed-forbidden",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-auth-failed-403",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"description": "Synthetic noncanonical HTTP 200 authorization text is incompatible rather than token-rejection 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": "authorization-failed-near-miss",
|
||||
"method": "GET",
|
||||
"operation": "authorization-failed-near-miss",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_ordered_list",
|
||||
"type": "vod"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"error": "Authorization failed."
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "authorization-failed-near-miss",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-authorization-failed-near-miss-200",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"description": "Synthetic string profile statuses preserve the same ready, blocked, and credential-required meanings as numeric statuses.",
|
||||
"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": "profile-string-ready",
|
||||
"method": "GET",
|
||||
"operation": "profile-string-ready",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_profile",
|
||||
"auth_second_step": "0",
|
||||
"case": "string-zero",
|
||||
"type": "stb"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"id": 1,
|
||||
"status": "0",
|
||||
"store_auth_data_on_stb": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"cardinality": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
},
|
||||
"id": "profile-string-blocked",
|
||||
"method": "GET",
|
||||
"operation": "profile-string-blocked",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_profile",
|
||||
"auth_second_step": "0",
|
||||
"case": "string-one",
|
||||
"type": "stb"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"block_msg": "Synthetic blocked profile",
|
||||
"status": "1",
|
||||
"store_auth_data_on_stb": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"cardinality": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
},
|
||||
"id": "profile-string-credentials",
|
||||
"method": "GET",
|
||||
"operation": "profile-string-credentials",
|
||||
"origin": "portal",
|
||||
"path": "/portal.php",
|
||||
"request": {
|
||||
"body": {
|
||||
"kind": "absent"
|
||||
},
|
||||
"cookies": {
|
||||
"absent": [],
|
||||
"attributes": {},
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"headers": {
|
||||
"absent": [],
|
||||
"exact": {},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [],
|
||||
"exact": {
|
||||
"action": "get_profile",
|
||||
"auth_second_step": "0",
|
||||
"case": "string-two",
|
||||
"type": "stb"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"js": {
|
||||
"credentials_required": true,
|
||||
"status": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "profile-status-strings",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-profile-status-strings",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"description": "Synthetic valid JSON labeled as HTML fails closed instead of being compatibility-sniffed.",
|
||||
"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": "wrong-mime-json",
|
||||
"method": "GET",
|
||||
"operation": "wrong-mime-json",
|
||||
"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": "{\"js\":{\"status\":0}}"
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"text/html"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "wrong-mime-json",
|
||||
"nextState": "complete",
|
||||
"state": "start"
|
||||
}
|
||||
],
|
||||
"scenarioId": "classifier-wrong-mime-json",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"description": "Synthetic early stateless evidence cannot be selected after a later candidate exposes portal-protection evidence.",
|
||||
"entry": {
|
||||
"origin": "portal",
|
||||
"path": "/"
|
||||
},
|
||||
"expectedEndpoint": {
|
||||
"origin": "portal",
|
||||
"path": "/portal.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": "unsupported-handshake",
|
||||
"method": "GET",
|
||||
"operation": "unsupported-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": {
|
||||
"action": "handshake",
|
||||
"type": "stb"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "json",
|
||||
"value": {
|
||||
"error": "synthetic-unsupported"
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 404
|
||||
}
|
||||
},
|
||||
{
|
||||
"cardinality": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
},
|
||||
"id": "stateless-capability",
|
||||
"method": "GET",
|
||||
"operation": "stateless-capability",
|
||||
"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": {
|
||||
"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": "protected-candidate",
|
||||
"state": "stateless-candidate"
|
||||
},
|
||||
{
|
||||
"expectations": [
|
||||
{
|
||||
"cardinality": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
},
|
||||
"id": "authenticated-handshake",
|
||||
"method": "GET",
|
||||
"operation": "authenticated-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": "authenticated-token",
|
||||
"valueKind": "token"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"application/json"
|
||||
]
|
||||
},
|
||||
"status": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"cardinality": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
},
|
||||
"id": "protected-first-profile",
|
||||
"method": "GET",
|
||||
"operation": "protected-first-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": "authenticated-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"present": []
|
||||
},
|
||||
"query": {
|
||||
"absent": [
|
||||
"password",
|
||||
"username"
|
||||
],
|
||||
"exact": {
|
||||
"action": "get_profile",
|
||||
"auth_second_step": "0",
|
||||
"type": "stb"
|
||||
},
|
||||
"present": []
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"body": {
|
||||
"kind": "text",
|
||||
"value": "Synthetic Web Application Firewall challenge"
|
||||
},
|
||||
"headers": {
|
||||
"content-type": [
|
||||
"text/html"
|
||||
]
|
||||
},
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
],
|
||||
"mode": "ordered",
|
||||
"name": "protected-candidate",
|
||||
"nextState": "complete",
|
||||
"state": "protected-candidate"
|
||||
}
|
||||
],
|
||||
"scenarioId": "resolver-downgrade-protection",
|
||||
"schemaVersion": 1,
|
||||
"symbols": [
|
||||
{
|
||||
"kind": "generate",
|
||||
"symbol": "mac",
|
||||
"valueKind": "mac"
|
||||
}
|
||||
],
|
||||
"terminalState": "complete"
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
/* 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 { classifyStalkerDoAuth } from '@iptvnator/portal/stalker/protocol';
|
||||
import {
|
||||
classifyStalkerDoAuth,
|
||||
classifyStalkerProfile,
|
||||
classifyStalkerResponseFailure,
|
||||
parseStalkerResponseEnvelope,
|
||||
} from '@iptvnator/portal/stalker/protocol';
|
||||
import { createReplayRun, type ReplayRun } from './replay-run.js';
|
||||
import { parseReplayFixtureText } from './replay-schema.js';
|
||||
import type {
|
||||
@@ -32,10 +37,16 @@ const EXPECTED_SCENARIOS = [
|
||||
'authentication-status2-second-step',
|
||||
'authentication-three-attempt-limit',
|
||||
'classifier-access-denied-200',
|
||||
'classifier-ambiguous-403',
|
||||
'classifier-auth-failed-403',
|
||||
'classifier-authorization-failed-200',
|
||||
'classifier-authorization-failed-near-miss-200',
|
||||
'classifier-oversized-response',
|
||||
'classifier-profile-status-strings',
|
||||
'classifier-rate-limit-429',
|
||||
'classifier-service-unavailable-503',
|
||||
'classifier-waf-403',
|
||||
'classifier-wrong-mime-json',
|
||||
'cookies-managed-shadow',
|
||||
'cookies-session-isolation',
|
||||
'e2e-concurrent-catalog-refresh',
|
||||
@@ -50,6 +61,7 @@ const EXPECTED_SCENARIOS = [
|
||||
'resolver-custom-prefix-full',
|
||||
'resolver-direct-load-jsonp',
|
||||
'resolver-direct-portal-statusless',
|
||||
'resolver-downgrade-protection',
|
||||
'resolver-learned-rediscovery',
|
||||
'resolver-root-full-wins',
|
||||
'resolver-root-landing',
|
||||
@@ -68,6 +80,31 @@ const RESERVED_EARLY_QUERY_FIELDS = [
|
||||
] as const;
|
||||
const RESERVED_EARLY_HEADERS = ['authorization', 'cookie'] as const;
|
||||
const RESERVED_EARLY_COOKIES = ['mac', 'session'] as const;
|
||||
const UNIT_OWNED_COOKIE_MATRIX = [
|
||||
{
|
||||
contract:
|
||||
'RFC Domain, Path, Secure, HttpOnly, and duplicate-name selection',
|
||||
evidence:
|
||||
'applies RFC domain, path, secure, HttpOnly, and duplicate-name rules',
|
||||
spec: 'apps/electron-backend/src/app/services/stalker-session/stalker-cookie-jar.spec.ts',
|
||||
},
|
||||
{
|
||||
contract: 'cookie expiry against a deterministic clock',
|
||||
evidence: 'honors expiry against an injected clock',
|
||||
spec: 'apps/electron-backend/src/app/services/stalker-session/stalker-cookie-jar.spec.ts',
|
||||
},
|
||||
{
|
||||
contract: 'public-suffix Domain rejection',
|
||||
evidence: 'rejects public-suffix cookies without retaining them',
|
||||
spec: 'apps/electron-backend/src/app/services/stalker-session/stalker-cookie-jar.spec.ts',
|
||||
},
|
||||
{
|
||||
contract: 'redirect-hop cookie collection and rotation',
|
||||
evidence:
|
||||
'prepares and collects the cookie jar on every same-origin hop without flattening Set-Cookie arrays',
|
||||
spec: 'apps/electron-backend/src/app/services/stalker-session/stalker-http-session.spec.ts',
|
||||
},
|
||||
] as const;
|
||||
|
||||
function collectFixturePaths(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true })
|
||||
@@ -594,6 +631,57 @@ function assertAnonymousRequestHasNoReservedSecrets(
|
||||
);
|
||||
}
|
||||
|
||||
function loadFixture(scenarioId: string): ReplayFixtureV1 {
|
||||
for (const fixturePath of collectFixturePaths(FIXTURE_ROOT)) {
|
||||
const fixture = parseReplayFixtureText(
|
||||
readFileSync(fixturePath, 'utf8')
|
||||
);
|
||||
if (fixture.scenarioId === scenarioId) {
|
||||
return fixture;
|
||||
}
|
||||
}
|
||||
throw new Error(`Replay matrix fixture missing: ${scenarioId}.`);
|
||||
}
|
||||
|
||||
function findExpectation(
|
||||
fixture: ReplayFixtureV1,
|
||||
operation: string
|
||||
): ReplayExpectation {
|
||||
const expectation = fixture.phases
|
||||
.flatMap((phase) => phase.expectations)
|
||||
.find((candidate) => candidate.operation === operation);
|
||||
if (expectation === undefined) {
|
||||
throw new Error(
|
||||
`Replay matrix operation missing: ${fixture.scenarioId}/${operation}.`
|
||||
);
|
||||
}
|
||||
return expectation;
|
||||
}
|
||||
|
||||
function jsonResponseValue(expectation: ReplayExpectation): unknown {
|
||||
if (expectation.response.body.kind !== 'json') {
|
||||
throw new Error(
|
||||
`Replay matrix response is not JSON: ${expectation.operation}.`
|
||||
);
|
||||
}
|
||||
return expectation.response.body.value;
|
||||
}
|
||||
|
||||
function responseFailureInput(expectation: ReplayExpectation): {
|
||||
readonly httpStatus: number;
|
||||
readonly rawBody?: string;
|
||||
readonly value?: unknown;
|
||||
} {
|
||||
const body = expectation.response.body;
|
||||
return {
|
||||
httpStatus: expectation.response.status,
|
||||
...(body.kind === 'json' ? { value: body.value } : {}),
|
||||
...(body.kind === 'text' && typeof body.value === 'string'
|
||||
? { rawBody: body.value }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe('committed replay fixture corpus', () => {
|
||||
it('contains the complete Stage-1 scenario matrix', () => {
|
||||
const scenarioIds = collectFixturePaths(FIXTURE_ROOT)
|
||||
@@ -645,6 +733,302 @@ describe('committed replay fixture corpus', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('binds numeric and string profile statuses to the production classifier matrix', () => {
|
||||
const cases = [
|
||||
{
|
||||
expected: { kind: 'ready', status: 0 },
|
||||
operation: 'full-profile',
|
||||
wireStatus: 0,
|
||||
scenarioId: 'resolver-root-full-wins',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'blocked', status: 1 },
|
||||
operation: 'blocked-profile',
|
||||
wireStatus: 1,
|
||||
scenarioId: 'authentication-blocked-profile',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'credentials-required', status: 2 },
|
||||
operation: 'profile-first',
|
||||
wireStatus: 2,
|
||||
scenarioId: 'authentication-status2-second-step',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'ready', status: 0 },
|
||||
operation: 'profile-string-ready',
|
||||
wireStatus: '0',
|
||||
scenarioId: 'classifier-profile-status-strings',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'blocked', status: 1 },
|
||||
operation: 'profile-string-blocked',
|
||||
wireStatus: '1',
|
||||
scenarioId: 'classifier-profile-status-strings',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'credentials-required', status: 2 },
|
||||
operation: 'profile-string-credentials',
|
||||
wireStatus: '2',
|
||||
scenarioId: 'classifier-profile-status-strings',
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const current of cases) {
|
||||
const response = jsonResponseValue(
|
||||
findExpectation(
|
||||
loadFixture(current.scenarioId),
|
||||
current.operation
|
||||
)
|
||||
) as { readonly js?: Readonly<Record<string, unknown>> };
|
||||
expect(response.js?.['status']).toBe(current.wireStatus);
|
||||
expect(classifyStalkerProfile(response)).toMatchObject(
|
||||
current.expected
|
||||
);
|
||||
}
|
||||
|
||||
const stringFixture = loadFixture(
|
||||
'classifier-profile-status-strings'
|
||||
);
|
||||
const ready = jsonResponseValue(
|
||||
findExpectation(stringFixture, 'profile-string-ready')
|
||||
) as { readonly js: Readonly<Record<string, unknown>> };
|
||||
const blocked = jsonResponseValue(
|
||||
findExpectation(stringFixture, 'profile-string-blocked')
|
||||
) as { readonly js: Readonly<Record<string, unknown>> };
|
||||
expect([
|
||||
ready.js['store_auth_data_on_stb'],
|
||||
blocked.js['store_auth_data_on_stb'],
|
||||
]).toEqual([true, false]);
|
||||
});
|
||||
|
||||
it('binds canonical do_auth success, rejection, and near misses to production rules', () => {
|
||||
const cases = [
|
||||
{
|
||||
expected: { kind: 'success' },
|
||||
operation: 'do-auth',
|
||||
scenarioId: 'authentication-status2-second-step',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'credentials-rejected' },
|
||||
operation: 'saved-do-auth',
|
||||
scenarioId: 'authentication-saved-rejected-fresh',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'incompatible-response',
|
||||
},
|
||||
operation: 'do-auth-noncanonical',
|
||||
scenarioId: 'authentication-noncanonical-do-auth',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'incompatible-response',
|
||||
},
|
||||
operation: 'do-auth-noncanonical-string',
|
||||
scenarioId: 'authentication-noncanonical-do-auth-string',
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const current of cases) {
|
||||
expect(
|
||||
classifyStalkerDoAuth(
|
||||
jsonResponseValue(
|
||||
findExpectation(
|
||||
loadFixture(current.scenarioId),
|
||||
current.operation
|
||||
)
|
||||
)
|
||||
)
|
||||
).toEqual(current.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('binds token, denial, protection, and ambiguous bodies to the production failure taxonomy', () => {
|
||||
const cases = [
|
||||
{
|
||||
expected: { kind: 'token-rejected' },
|
||||
operation: 'authorization-failed',
|
||||
scenarioId: 'classifier-authorization-failed-200',
|
||||
},
|
||||
{
|
||||
expected: { kind: 'token-rejected' },
|
||||
operation: 'authorization-failed-forbidden',
|
||||
scenarioId: 'classifier-auth-failed-403',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'incompatible-response',
|
||||
},
|
||||
operation: 'authorization-failed-near-miss',
|
||||
scenarioId:
|
||||
'classifier-authorization-failed-near-miss-200',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'account-access-denied',
|
||||
},
|
||||
operation: 'body-denial',
|
||||
scenarioId: 'classifier-access-denied-200',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'portal-protection-blocked',
|
||||
},
|
||||
operation: 'waf-response',
|
||||
scenarioId: 'classifier-waf-403',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'incompatible-response',
|
||||
},
|
||||
operation: 'ambiguous-forbidden',
|
||||
scenarioId: 'classifier-ambiguous-403',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'rate-limited',
|
||||
},
|
||||
operation: 'rate-limit',
|
||||
scenarioId: 'classifier-rate-limit-429',
|
||||
},
|
||||
{
|
||||
expected: {
|
||||
kind: 'failure',
|
||||
reason: 'portal-unavailable',
|
||||
},
|
||||
operation: 'service-unavailable',
|
||||
scenarioId: 'classifier-service-unavailable-503',
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const current of cases) {
|
||||
const expectation = findExpectation(
|
||||
loadFixture(current.scenarioId),
|
||||
current.operation
|
||||
);
|
||||
expect(
|
||||
classifyStalkerResponseFailure(
|
||||
responseFailureInput(expectation)
|
||||
)
|
||||
).toEqual(current.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('fails closed when valid replay JSON carries an HTML media type', () => {
|
||||
const expectation = findExpectation(
|
||||
loadFixture('classifier-wrong-mime-json'),
|
||||
'wrong-mime-json'
|
||||
);
|
||||
const body = expectation.response.body;
|
||||
const contentType =
|
||||
expectation.response.headers['content-type']?.[0];
|
||||
if (
|
||||
body.kind !== 'text' ||
|
||||
typeof body.value !== 'string' ||
|
||||
typeof contentType !== 'string'
|
||||
) {
|
||||
throw new Error('Wrong-MIME matrix fixture is malformed.');
|
||||
}
|
||||
|
||||
expect(
|
||||
parseStalkerResponseEnvelope({
|
||||
body: body.value,
|
||||
contentType,
|
||||
maxBodyBytes: 1024,
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'failure',
|
||||
reason: 'incompatible-response',
|
||||
});
|
||||
});
|
||||
|
||||
it('records portal protection after stateless evidence as a no-downgrade terminal', () => {
|
||||
const fixture = loadFixture('resolver-downgrade-protection');
|
||||
const operations = fixture.phases.flatMap((phase) =>
|
||||
phase.expectations.map((expectation) => expectation.operation)
|
||||
);
|
||||
const unsupported = findExpectation(
|
||||
fixture,
|
||||
'unsupported-handshake'
|
||||
);
|
||||
const stateless = jsonResponseValue(
|
||||
findExpectation(fixture, 'stateless-capability')
|
||||
) as { readonly js?: unknown };
|
||||
const authenticatedHandshake = jsonResponseValue(
|
||||
findExpectation(fixture, 'authenticated-handshake')
|
||||
) as {
|
||||
readonly js?: Readonly<Record<string, unknown>>;
|
||||
};
|
||||
const protectedProfile = findExpectation(
|
||||
fixture,
|
||||
'protected-first-profile'
|
||||
);
|
||||
|
||||
expect(operations).toEqual([
|
||||
'root-probe',
|
||||
'unsupported-handshake',
|
||||
'stateless-capability',
|
||||
'authenticated-handshake',
|
||||
'protected-first-profile',
|
||||
]);
|
||||
expect(
|
||||
classifyStalkerResponseFailure(
|
||||
responseFailureInput(unsupported)
|
||||
)
|
||||
).toEqual({ kind: 'none' });
|
||||
expect(Array.isArray(stateless.js)).toBe(true);
|
||||
expect(authenticatedHandshake.js?.['token']).toMatchObject({
|
||||
kind: 'generate',
|
||||
valueKind: 'token',
|
||||
});
|
||||
expect(
|
||||
classifyStalkerResponseFailure(
|
||||
responseFailureInput(protectedProfile)
|
||||
)
|
||||
).toEqual({
|
||||
kind: 'failure',
|
||||
reason: 'portal-protection-blocked',
|
||||
});
|
||||
expect(fixture.phases.at(-1)?.nextState).toBe(
|
||||
fixture.terminalState
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps mutable RFC cookie semantics in production unit coverage while replay owns wire rotation', () => {
|
||||
const rotationFixture = loadFixture(
|
||||
'e2e-full-session-catalog-playback'
|
||||
);
|
||||
const rotatingOperations = ['handshake', 'do-auth', 'catalog-page'];
|
||||
for (const operation of rotatingOperations) {
|
||||
expect(
|
||||
findExpectation(rotationFixture, operation).response.headers[
|
||||
'set-cookie'
|
||||
]
|
||||
).toHaveLength(1);
|
||||
}
|
||||
|
||||
for (const current of UNIT_OWNED_COOKIE_MATRIX) {
|
||||
const specSource = readFileSync(
|
||||
resolve(process.cwd(), current.spec),
|
||||
'utf8'
|
||||
);
|
||||
expect({
|
||||
contract: current.contract,
|
||||
covered: specSource.includes(current.evidence),
|
||||
}).toEqual({
|
||||
contract: current.contract,
|
||||
covered: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('terminates both noncanonical do_auth near misses before a second profile', () => {
|
||||
const cases = [
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user