Add Playwright portal regression and harden direct auth
This commit is contained in:
parent
e57f8a446f
commit
bd839ea42d
|
|
@ -4,6 +4,9 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
tls/
|
tls/
|
||||||
|
node_modules/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
# Keep firmware SDK tree out of this workspace-tracking repo
|
# Keep firmware SDK tree out of this workspace-tracking repo
|
||||||
CR_SDK_CK-main/
|
CR_SDK_CK-main/
|
||||||
|
|
|
||||||
|
|
@ -187,11 +187,34 @@ REQUESTS=50 PARALLELISM=12 /home/user/chromecard/phase5_chain_regression.sh
|
||||||
/home/user/chromecard/phase5_chain_regression.sh --username alice --client-host k_client
|
/home/user/chromecard/phase5_chain_regression.sh --username alice --client-host k_client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For the browser-facing `k_client` page, use the Playwright regression spec:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npx playwright install
|
||||||
|
npm run test:k-client
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- default target is `http://127.0.0.1:8766`
|
||||||
|
- override with `PORTAL_BASE_URL=http://127.0.0.1:8766`
|
||||||
|
- the spec expects manual card confirmation during register and login
|
||||||
|
- timeouts can be tuned with `CARD_REGISTRATION_TIMEOUT_MS` and `CARD_LOGIN_TIMEOUT_MS`
|
||||||
|
- from this host, a forwarded portal URL was used successfully:
|
||||||
|
- `PORTAL_BASE_URL=http://127.0.0.1:18766 npm run test:k-client`
|
||||||
|
|
||||||
Verified result on 2026-04-25:
|
Verified result on 2026-04-25:
|
||||||
|
|
||||||
- Live split-VM chain passed end-to-end.
|
- Live split-VM chain passed end-to-end.
|
||||||
- Login, session status, counter reuse, and logout all worked from `k_client`.
|
- Login, session status, counter reuse, and logout all worked from `k_client`.
|
||||||
- A `20` request / `8` worker concurrency burst returned unique, gap-free counter values `23..42`.
|
- A `20` request / `8` worker concurrency burst returned unique, gap-free counter values `23..42`.
|
||||||
|
- The Playwright browser regression for `k_client_portal.py` also passed end-to-end:
|
||||||
|
- register
|
||||||
|
- login
|
||||||
|
- protected counter
|
||||||
|
- logout
|
||||||
|
- unregister
|
||||||
|
|
||||||
## Current Limitation
|
## Current Limitation
|
||||||
|
|
||||||
|
|
|
||||||
7
Setup.md
7
Setup.md
|
|
@ -371,6 +371,13 @@ Session note (2026-04-25, k_client browser flow page):
|
||||||
- lets the operator select a listed user into the username field
|
- lets the operator select a listed user into the username field
|
||||||
- lets the operator unregister users from the browser page
|
- lets the operator unregister users from the browser page
|
||||||
- login now uses the current username field instead of only the portal's last remembered user
|
- login now uses the current username field instead of only the portal's last remembered user
|
||||||
|
- Added a browser regression harness for the `k_client` page:
|
||||||
|
- `/home/user/chromecard/tests/k_client_portal.spec.js`
|
||||||
|
- `/home/user/chromecard/playwright.config.js`
|
||||||
|
- `/home/user/chromecard/package.json`
|
||||||
|
- intended flow: register, login, call `k_server`, logout, unregister
|
||||||
|
- verified passing live on 2026-04-25 from this host via forwarded portal URL:
|
||||||
|
- `PORTAL_BASE_URL=http://127.0.0.1:18766 npm run test:k-client`
|
||||||
- It also makes the negative path explicit:
|
- It also makes the negative path explicit:
|
||||||
- if login is denied on the card, the page reports that `k_server` was not called
|
- if login is denied on the card, the page reports that `k_server` was not called
|
||||||
- Primary browser-facing app logic still lives on `k_proxy`, but the `k_client` page is now a concrete demo/control surface rather than just a redirect.
|
- Primary browser-facing app logic still lives on `k_proxy`, but the `k_client` page is now a concrete demo/control surface rather than just a redirect.
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,8 @@ Status (2026-04-25):
|
||||||
- the `k_client` page now also lists registered users from `k_proxy`
|
- the `k_client` page now also lists registered users from `k_proxy`
|
||||||
- the `k_client` page can unregister users from the browser
|
- the `k_client` page can unregister users from the browser
|
||||||
- the portal login action now uses the current username field instead of only the remembered local user
|
- the portal login action now uses the current username field instead of only the remembered local user
|
||||||
|
- a Playwright regression spec now exists for the browser flow in `tests/k_client_portal.spec.js`
|
||||||
|
- the Playwright browser regression has now passed end-to-end once from this host against a forwarded portal URL
|
||||||
- Verified end-to-end through the portal:
|
- Verified end-to-end through the portal:
|
||||||
- enroll `alice`
|
- enroll `alice`
|
||||||
- login succeeds
|
- login succeeds
|
||||||
|
|
|
||||||
|
|
@ -550,10 +550,19 @@ class EnrollmentRecord:
|
||||||
|
|
||||||
|
|
||||||
class ClientState:
|
class ClientState:
|
||||||
def __init__(self, proxy_base_url: str, proxy_ca_file: str | None, enroll_db: Path):
|
def __init__(
|
||||||
|
self,
|
||||||
|
proxy_base_url: str,
|
||||||
|
proxy_ca_file: str | None,
|
||||||
|
enroll_db: Path,
|
||||||
|
interactive_timeout_s: float = 90.0,
|
||||||
|
default_timeout_s: float = 10.0,
|
||||||
|
):
|
||||||
self.proxy_base_url = proxy_base_url.rstrip("/")
|
self.proxy_base_url = proxy_base_url.rstrip("/")
|
||||||
self.proxy_ca_file = proxy_ca_file
|
self.proxy_ca_file = proxy_ca_file
|
||||||
self.enroll_db = enroll_db
|
self.enroll_db = enroll_db
|
||||||
|
self.interactive_timeout_s = interactive_timeout_s
|
||||||
|
self.default_timeout_s = default_timeout_s
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.preferred_enrollment: EnrollmentRecord | None = None
|
self.preferred_enrollment: EnrollmentRecord | None = None
|
||||||
self.session_token: str | None = None
|
self.session_token: str | None = None
|
||||||
|
|
@ -565,7 +574,14 @@ class ClientState:
|
||||||
return ssl.create_default_context(cafile=self.proxy_ca_file)
|
return ssl.create_default_context(cafile=self.proxy_ca_file)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _proxy_json(self, method: str, path: str, payload: dict[str, Any] | None = None) -> tuple[int, dict[str, Any]]:
|
def _proxy_json(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
timeout_s: float | None = None,
|
||||||
|
) -> tuple[int, dict[str, Any]]:
|
||||||
req = Request(f"{self.proxy_base_url}{path}", method=method)
|
req = Request(f"{self.proxy_base_url}{path}", method=method)
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
token = self.get_session_token()
|
token = self.get_session_token()
|
||||||
|
|
@ -573,7 +589,12 @@ class ClientState:
|
||||||
req.add_header("Authorization", f"Bearer {token}")
|
req.add_header("Authorization", f"Bearer {token}")
|
||||||
body = json.dumps(payload or {}).encode("utf-8")
|
body = json.dumps(payload or {}).encode("utf-8")
|
||||||
try:
|
try:
|
||||||
with urlopen(req, data=body, timeout=10, context=self._ssl_context()) as resp:
|
with urlopen(
|
||||||
|
req,
|
||||||
|
data=body,
|
||||||
|
timeout=timeout_s or self.default_timeout_s,
|
||||||
|
context=self._ssl_context(),
|
||||||
|
) as resp:
|
||||||
return resp.status, json.loads(resp.read().decode("utf-8"))
|
return resp.status, json.loads(resp.read().decode("utf-8"))
|
||||||
except HTTPError as exc:
|
except HTTPError as exc:
|
||||||
try:
|
try:
|
||||||
|
|
@ -605,7 +626,12 @@ class ClientState:
|
||||||
username = username.strip()
|
username = username.strip()
|
||||||
if not username:
|
if not username:
|
||||||
return {"ok": False, "error": "username required"}
|
return {"ok": False, "error": "username required"}
|
||||||
status, data = self._proxy_json("POST", "/enroll/register", {"username": username})
|
status, data = self._proxy_json(
|
||||||
|
"POST",
|
||||||
|
"/enroll/register",
|
||||||
|
{"username": username},
|
||||||
|
timeout_s=self.interactive_timeout_s,
|
||||||
|
)
|
||||||
if status != 200:
|
if status != 200:
|
||||||
return data
|
return data
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
@ -660,7 +686,12 @@ class ClientState:
|
||||||
else:
|
else:
|
||||||
return 400, {"ok": False, "error": "no enrolled user"}
|
return 400, {"ok": False, "error": "no enrolled user"}
|
||||||
|
|
||||||
status, data = self._proxy_json("POST", "/session/login", {"username": username})
|
status, data = self._proxy_json(
|
||||||
|
"POST",
|
||||||
|
"/session/login",
|
||||||
|
{"username": username},
|
||||||
|
timeout_s=self.interactive_timeout_s,
|
||||||
|
)
|
||||||
if status == 200 and data.get("session_token"):
|
if status == 200 and data.get("session_token"):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.preferred_enrollment = EnrollmentRecord(username=username)
|
self.preferred_enrollment = EnrollmentRecord(username=username)
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,8 @@ class ProxyState:
|
||||||
self.rp_id = rp_id
|
self.rp_id = rp_id
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.direct_device_path = direct_device_path
|
self.direct_device_path = direct_device_path
|
||||||
|
self.direct_device_configured_path = direct_device_path
|
||||||
|
self.direct_device_active_path: str | None = None
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.direct_device_lock = threading.RLock()
|
self.direct_device_lock = threading.RLock()
|
||||||
self.direct_device: CtapHidDevice | None = None
|
self.direct_device: CtapHidDevice | None = None
|
||||||
|
|
@ -616,7 +618,7 @@ class ProxyState:
|
||||||
return Fido2Client(device, self.origin, verify=verify_rp_id, user_interaction=ProxyUserInteraction())
|
return Fido2Client(device, self.origin, verify=verify_rp_id, user_interaction=ProxyUserInteraction())
|
||||||
|
|
||||||
def _direct_device_candidates(self) -> list[str]:
|
def _direct_device_candidates(self) -> list[str]:
|
||||||
configured = str(self.direct_device_path).strip()
|
configured = str(self.direct_device_configured_path).strip()
|
||||||
candidates: list[str] = []
|
candidates: list[str] = []
|
||||||
if configured:
|
if configured:
|
||||||
candidates.append(configured)
|
candidates.append(configured)
|
||||||
|
|
@ -628,13 +630,20 @@ class ProxyState:
|
||||||
|
|
||||||
def _open_direct_device(self) -> CtapHidDevice:
|
def _open_direct_device(self) -> CtapHidDevice:
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
|
recoverable: tuple[type[Exception], ...] = (FileNotFoundError, PermissionError)
|
||||||
for candidate in self._direct_device_candidates():
|
for candidate in self._direct_device_candidates():
|
||||||
try:
|
try:
|
||||||
descriptor = get_descriptor(candidate)
|
descriptor = get_descriptor(candidate)
|
||||||
device = CtapHidDevice(descriptor, open_connection(descriptor))
|
device = CtapHidDevice(descriptor, open_connection(descriptor))
|
||||||
self.direct_device_path = candidate
|
self.direct_device_active_path = candidate
|
||||||
return device
|
return device
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
# USB re-enumeration can leave stale hidraw paths behind, and some sibling
|
||||||
|
# nodes are vendor interfaces that are not readable to the normal user.
|
||||||
|
# Skip those and keep probing for a usable CTAPHID node.
|
||||||
|
if isinstance(exc, recoverable):
|
||||||
|
last_exc = exc
|
||||||
|
continue
|
||||||
last_exc = exc
|
last_exc = exc
|
||||||
if last_exc is None:
|
if last_exc is None:
|
||||||
raise FileNotFoundError(f"no hidraw devices available for direct auth (configured {self.direct_device_path})")
|
raise FileNotFoundError(f"no hidraw devices available for direct auth (configured {self.direct_device_path})")
|
||||||
|
|
@ -643,15 +652,24 @@ class ProxyState:
|
||||||
def _get_direct_device(self, *, force_reopen: bool = False) -> CtapHidDevice:
|
def _get_direct_device(self, *, force_reopen: bool = False) -> CtapHidDevice:
|
||||||
with self.direct_device_lock:
|
with self.direct_device_lock:
|
||||||
if force_reopen and self.direct_device is not None:
|
if force_reopen and self.direct_device is not None:
|
||||||
try:
|
self._drop_direct_device_locked()
|
||||||
self.direct_device.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.direct_device = None
|
|
||||||
if self.direct_device is None:
|
if self.direct_device is None:
|
||||||
self.direct_device = self._open_direct_device()
|
self.direct_device = self._open_direct_device()
|
||||||
return self.direct_device
|
return self.direct_device
|
||||||
|
|
||||||
|
def _drop_direct_device_locked(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.direct_device is not None:
|
||||||
|
self.direct_device.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.direct_device = None
|
||||||
|
self.direct_device_active_path = None
|
||||||
|
|
||||||
|
def _drop_direct_device(self) -> None:
|
||||||
|
with self.direct_device_lock:
|
||||||
|
self._drop_direct_device_locked()
|
||||||
|
|
||||||
def _with_direct_ctap2(self, action):
|
def _with_direct_ctap2(self, action):
|
||||||
with self.direct_device_lock:
|
with self.direct_device_lock:
|
||||||
last_exc: Exception | None = None
|
last_exc: Exception | None = None
|
||||||
|
|
@ -661,12 +679,7 @@ class ProxyState:
|
||||||
return action(Ctap2(device))
|
return action(Ctap2(device))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
last_exc = exc
|
last_exc = exc
|
||||||
try:
|
self._drop_direct_device_locked()
|
||||||
if self.direct_device is not None:
|
|
||||||
self.direct_device.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.direct_device = None
|
|
||||||
assert last_exc is not None
|
assert last_exc is not None
|
||||||
raise last_exc
|
raise last_exc
|
||||||
|
|
||||||
|
|
@ -768,6 +781,9 @@ class ProxyState:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.enrollments[canonical] = enrollment
|
self.enrollments[canonical] = enrollment
|
||||||
self._save_enrollments_locked()
|
self._save_enrollments_locked()
|
||||||
|
# Freshly reopen for later assertion flow; some cards do not like immediate
|
||||||
|
# reuse of the same hidraw handle across makeCredential -> getAssertion.
|
||||||
|
self._drop_direct_device()
|
||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
def register_enrollment(self, username: str, display_name: str | None) -> Enrollment:
|
def register_enrollment(self, username: str, display_name: str | None) -> Enrollment:
|
||||||
|
|
@ -843,6 +859,9 @@ class ProxyState:
|
||||||
if not enrollment.credential_data_b64:
|
if not enrollment.credential_data_b64:
|
||||||
return False, "user has no registered credential"
|
return False, "user has no registered credential"
|
||||||
try:
|
try:
|
||||||
|
# Start assertion from a fresh device open rather than reusing the
|
||||||
|
# post-registration handle, which has been flaky on this stack.
|
||||||
|
self._drop_direct_device()
|
||||||
credential = AttestedCredentialData(b64u_decode(enrollment.credential_data_b64))
|
credential = AttestedCredentialData(b64u_decode(enrollment.credential_data_b64))
|
||||||
# Keep UV explicitly discouraged here. On the current card/library stack,
|
# Keep UV explicitly discouraged here. On the current card/library stack,
|
||||||
# asking for stronger UV flows immediately trips PIN/UV capability errors.
|
# asking for stronger UV flows immediately trips PIN/UV capability errors.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"name": "chromecard-browser-regression",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "chromecard-browser-regression",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.54.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.59.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.59.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||||
|
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "chromecard-browser-regression",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Playwright regression checks for the k_client browser flow",
|
||||||
|
"scripts": {
|
||||||
|
"test:k-client": "playwright test tests/k_client_portal.spec.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.54.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Minimal local Playwright config for the k_client browser flow.
|
||||||
|
const { defineConfig } = require("@playwright/test");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: "./tests",
|
||||||
|
timeout: 180_000,
|
||||||
|
expect: {
|
||||||
|
timeout: 15_000,
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.PORTAL_BASE_URL || "http://127.0.0.1:8766",
|
||||||
|
headless: process.env.PW_HEADLESS === "1",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
|
},
|
||||||
|
reporter: [["list"]],
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
const { test, expect } = require("@playwright/test");
|
||||||
|
|
||||||
|
const registrationTimeoutMs = Number(process.env.CARD_REGISTRATION_TIMEOUT_MS || "90000");
|
||||||
|
const loginTimeoutMs = Number(process.env.CARD_LOGIN_TIMEOUT_MS || "90000");
|
||||||
|
|
||||||
|
function uniqueUsername() {
|
||||||
|
return `pw_${Date.now().toString(36)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForActionResult(page, action, expectedText, timeoutMs) {
|
||||||
|
const flowResult = page.locator("#flowResult");
|
||||||
|
await action();
|
||||||
|
await expect(flowResult).toContainText(expectedText, { timeout: timeoutMs });
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("k_client portal regression", () => {
|
||||||
|
test("registers, logs in, reads counter, logs out, and unregisters", async ({ page }) => {
|
||||||
|
const username = uniqueUsername();
|
||||||
|
const usersList = page.locator("#usersList");
|
||||||
|
const flowResult = page.locator("#flowResult");
|
||||||
|
const sessionLine = page.locator("#stateSession");
|
||||||
|
|
||||||
|
test.setTimeout(registrationTimeoutMs + loginTimeoutMs + 90_000);
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.getByRole("heading", { name: "ChromeCard Client Flow" })).toBeVisible();
|
||||||
|
await page.getByLabel("Username").fill(username);
|
||||||
|
|
||||||
|
await test.step("Register user", async () => {
|
||||||
|
// Card step: press yes on the registration prompt.
|
||||||
|
await waitForActionResult(
|
||||||
|
page,
|
||||||
|
() => page.getByRole("button", { name: "Register User" }).click(),
|
||||||
|
"User registration succeeded.",
|
||||||
|
registrationTimeoutMs
|
||||||
|
);
|
||||||
|
await expect(usersList).toContainText(username);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Login", async () => {
|
||||||
|
// Card step: press yes on the authentication prompt.
|
||||||
|
await waitForActionResult(
|
||||||
|
page,
|
||||||
|
() => page.getByRole("button", { name: "Login" }).click(),
|
||||||
|
"Login succeeded. You can now call k_server.",
|
||||||
|
loginTimeoutMs
|
||||||
|
);
|
||||||
|
await expect(sessionLine).toContainText("Session active: yes");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Call k_server counter", async () => {
|
||||||
|
await page.getByRole("button", { name: "Call k_server" }).click();
|
||||||
|
await expect(flowResult).toContainText("k_server was reached. Counter value:");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Logout", async () => {
|
||||||
|
await page.getByRole("button", { name: "Logout" }).click();
|
||||||
|
await expect(flowResult).toContainText("Session cleared.");
|
||||||
|
await expect(sessionLine).toContainText("Session active: no");
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Unregister user", async () => {
|
||||||
|
const row = usersList.locator(".user-row", { hasText: username });
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.getByRole("button", { name: "Unregister" }).click();
|
||||||
|
await expect(flowResult).toContainText(`User ${username} was unregistered.`);
|
||||||
|
await expect(usersList).not.toContainText(username);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue