Skip to content

Commit 773ec6f

Browse files
committed
Bug 2062629 - Add browser lock-on-restart marionette test
1 parent 11a0a70 commit 773ec6f

4 files changed

Lines changed: 212 additions & 40 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
import os
7+
import sys
8+
9+
sys.path.append(os.path.dirname(__file__))
10+
11+
from base_test import Environment
12+
from felt_tests import FeltTests
13+
14+
15+
class FeltLockTests(FeltTests):
16+
"""Shared setup for the FELT lock-on-close and lock-on-restart suites.
17+
18+
Both decide lock-vs-signout and inspect FELT-side persisted state, so they
19+
share keystore stubbing, sign-in, resume-token inspection, and settling.
20+
Subclasses provide their own _hold_felt_after_child_exit (the restart suite
21+
additionally forces the update path) plus their own trigger and assertions.
22+
"""
23+
24+
def _prepare_felt_keystore(self):
25+
"""Stub OSKeyStore on the FELT process and clear any leftover tokens.
26+
27+
Token encrypt/decrypt otherwise hits the real OS key store (a macOS
28+
Keychain password prompt), which stalls or fails headless; a reversible
29+
in-memory transform keeps the round-trip testable. Clearing felt.json
30+
(shared via UAppData, so it survives the per-test profile) prevents a
31+
prior test's token from turning this sign-in into an unlock."""
32+
driver = self.get_driver(Environment.FELT)
33+
driver.set_context("chrome")
34+
try:
35+
driver.execute_script(
36+
"""
37+
const { OSKeyStore } = ChromeUtils.importESModule(
38+
"resource://gre/modules/OSKeyStore.sys.mjs"
39+
);
40+
OSKeyStore.encrypt = async plaintext => "enc:" + plaintext;
41+
OSKeyStore.decrypt = async ciphertext =>
42+
String(ciphertext).replace(/^enc:/, "");
43+
44+
const { FeltStorage } = ChromeUtils.importESModule(
45+
"resource://gre/modules/enterprise/FeltStorage.sys.mjs"
46+
);
47+
if (FeltStorage._feltStorage?.data) {
48+
FeltStorage._feltStorage.data.lockingTokens = {};
49+
}
50+
"""
51+
)
52+
finally:
53+
driver.set_context("content")
54+
55+
def _felt_has_locking_token(self):
56+
"""Whether FELT persisted an encrypted resume token for the signed-in user.
57+
58+
Uses the synchronous hasLockingToken: getLockingToken is async and would
59+
return an always-truthy Promise through execute_script."""
60+
driver = self.get_driver(Environment.FELT)
61+
driver.set_context("chrome")
62+
try:
63+
return driver.execute_script(
64+
"""
65+
const { FeltStorage } = ChromeUtils.importESModule(
66+
"resource://gre/modules/enterprise/FeltStorage.sys.mjs"
67+
);
68+
const email = FeltStorage.getLastSignedInUser();
69+
return !!(email && FeltStorage.hasLockingToken(email));
70+
"""
71+
)
72+
finally:
73+
driver.set_context("content")
74+
75+
def _settle_after_child_exit(self, browser_pid):
76+
self.wait_process_exit(browser_pid)
77+
self.await_felt_auth_window()
78+
self.force_window()
79+
80+
def _start_signed_in(self):
81+
self._hold_felt_after_child_exit()
82+
self.run_felt_base()
83+
self._prepare_felt_keystore()
84+
self.connect_child_browser()
85+
self.assert_user_signed_in(env=Environment.FIREFOX)
86+
return self._child_driver.session_capabilities["moz:processID"]

testing/enterprise/manifest-browser.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ disabled = "Bug 1996558: scaffolding for the browsing-child encryption-mismatch
3333

3434
["test_felt_browser_lock_on_close.py"]
3535

36+
["test_felt_browser_lock_on_restart.py"]
37+
3638
["test_felt_browser_new_window_from_cli.py"]
3739

3840
["test_felt_browser_rapid_new_tabs_from_cli.py"]

testing/enterprise/test_felt_browser_lock_on_close.py

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
sys.path.append(os.path.dirname(__file__))
1010

1111
from base_test import Environment
12-
from felt_tests import FeltTests
12+
from felt_lock_tests import FeltLockTests
1313

1414
PREF_LOCK_ON_CLOSE = "enterprise.locking.browser_close"
1515
PREF_PROMPT_ON_SIGNOUT = "enterprise.prompt_on_signout"
1616

1717

18-
class BrowserLockOnClose(FeltTests):
18+
class BrowserLockOnClose(FeltLockTests):
1919
"""Verify the lock-vs-signout decision when closing a FELT browser.
2020
2121
The distinguishing signal is whether a server signout is posted (a lock
@@ -104,44 +104,28 @@ def _accept_close_dialog(self):
104104
"""
105105
)
106106

107-
def _felt_has_locking_token(self):
108-
"""Whether FELT persisted an encrypted resume token for the signed-in user."""
109-
driver = self.get_driver(Environment.FELT)
110-
driver.set_context("chrome")
111-
try:
112-
return driver.execute_script(
113-
"""
114-
const { FeltStorage } = ChromeUtils.importESModule(
115-
"resource://gre/modules/enterprise/FeltStorage.sys.mjs"
116-
);
117-
const email = FeltStorage.getLastSignedInUser();
118-
return !!(email && FeltStorage.getLockingToken(email));
119-
"""
120-
)
121-
finally:
122-
driver.set_context("content")
123-
124-
def _settle_after_close(self, browser_pid):
125-
self.wait_process_exit(browser_pid)
126-
self.await_felt_auth_window()
127-
self.force_window()
128-
129-
def _start_signed_in(self):
130-
self._hold_felt_after_child_exit()
131-
self.run_felt_base()
132-
self.connect_child_browser()
133-
self.assert_user_signed_in(env=Environment.FIREFOX)
134-
return self._child_driver.session_capabilities["moz:processID"]
107+
def _set_locking_enabled(self, enabled):
108+
"""enterprise.locking.browser_close is locked in firefox.js; Marionette
109+
can't override a locked pref the way the policy engine does (it unlocks
110+
first), so unlock it here to emulate the policy-applied value."""
111+
self._child_driver.set_context("chrome")
112+
self._child_driver.execute_script(
113+
"""
114+
const pref = arguments[0];
115+
Services.prefs.unlockPref(pref);
116+
Services.prefs.setBoolPref(pref, arguments[1]);
117+
""",
118+
script_args=(PREF_LOCK_ON_CLOSE, enabled),
119+
)
120+
self._child_driver.set_context("content")
135121

136122
def _begin_close_test(self, *, locking_enabled, prompt_enabled):
137123
"""Sign in, set the locking/prompt prefs, and assert no signout yet.
138124
139-
Returns the child browser pid for _settle_after_close."""
125+
Returns the child browser pid for _settle_after_child_exit."""
140126
browser_pid = self._start_signed_in()
141-
self._child_driver.set_prefs({
142-
PREF_LOCK_ON_CLOSE: locking_enabled,
143-
PREF_PROMPT_ON_SIGNOUT: prompt_enabled,
144-
})
127+
self._set_locking_enabled(locking_enabled)
128+
self._child_driver.set_pref(PREF_PROMPT_ON_SIGNOUT, prompt_enabled)
145129
assert self.signout_count.value == 0, "No signout should have been posted yet"
146130
return browser_pid
147131

@@ -169,16 +153,18 @@ def test_lock_on_close_persists_session_without_signout(self):
169153
browser_pid = self._begin_close_test(locking_enabled=True, prompt_enabled=False)
170154

171155
self._trigger_browser_closure()
172-
self._settle_after_close(browser_pid)
156+
self._settle_after_child_exit(browser_pid)
173157

174158
self._assert_locked()
175159

176160
def test_signout_on_close_when_locking_disabled(self):
177161
"""Locking disabled, prompt disabled: closing signs out (no token kept)."""
178-
browser_pid = self._begin_close_test(locking_enabled=False, prompt_enabled=False)
162+
browser_pid = self._begin_close_test(
163+
locking_enabled=False, prompt_enabled=False
164+
)
179165

180166
self._trigger_browser_closure()
181-
self._settle_after_close(browser_pid)
167+
self._settle_after_child_exit(browser_pid)
182168

183169
self._assert_signed_out()
184170

@@ -192,7 +178,7 @@ def test_prompt_lock_dialog_accept_locks(self):
192178
expected_reauth="You can resume your session after authenticating on this device.",
193179
)
194180
self._accept_close_dialog()
195-
self._settle_after_close(browser_pid)
181+
self._settle_after_child_exit(browser_pid)
196182

197183
self._assert_locked()
198184

@@ -206,6 +192,6 @@ def test_prompt_signout_dialog_accept_signs_out(self):
206192
expected_reauth="To use Firefox Enterprise again, you’ll need to reauthenticate through your organization’s SSO provider.",
207193
)
208194
self._accept_close_dialog()
209-
self._settle_after_close(browser_pid)
195+
self._settle_after_child_exit(browser_pid)
210196

211197
self._assert_signed_out()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env python3
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
import os
7+
import sys
8+
9+
sys.path.append(os.path.dirname(__file__))
10+
11+
from base_test import Environment
12+
from felt_lock_tests import FeltLockTests
13+
14+
PREF_FORCE_PENDING_UPDATE = "enterprise.felt_tests.force_pending_update"
15+
16+
17+
class BrowserLockOnRestart(FeltLockTests):
18+
"""Verify locking the FELT session on an update-driven restart.
19+
20+
Applying an update relaunches the whole application, so FELT must persist
21+
the session behind OS auth (rather than sign out) to resume it afterwards.
22+
Only the update path locks; a plain restart keeps the session in memory and
23+
is not exercised here. Since a real update-driven restart (signed MAR +
24+
relaunch) is unreachable under Marionette, the update path is forced with a
25+
test pref, and FELT is held after the child exits (is_blocking_shutdown) so
26+
the persisted state can be inspected in place of the real relaunch.
27+
"""
28+
29+
def _hold_felt_after_child_exit(self):
30+
# Keep FELT alive after the child exits so we can inspect FELT-side state,
31+
# and force the update path since a real update-driven restart is not
32+
# reachable under Marionette.
33+
self.get_driver(Environment.FELT).set_prefs(
34+
{
35+
"enterprise.felt_tests.should_not_close_window": True,
36+
"enterprise.felt_tests.is_blocking_shutdown": True,
37+
PREF_FORCE_PENDING_UPDATE: True,
38+
},
39+
default_branch=True,
40+
)
41+
42+
def _trigger_browser_restart(self):
43+
"""Restart the browser the way an update would: a plain eRestart quit.
44+
The browser's Felt IPC client observes it and tags the restart with the
45+
cached lock intent; no BrowserGlue interception is involved."""
46+
self._child_driver.set_context("chrome")
47+
self._manually_closed_child = True
48+
self._child_driver.execute_script(
49+
"""
50+
Services.startup.quit(
51+
Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
52+
);
53+
"""
54+
)
55+
56+
def _begin_restart_test(self, *, locking_enabled):
57+
"""Sign in, set the restart-lock intent, and assert no signout yet.
58+
59+
Returns the child browser pid for _settle_after_child_exit."""
60+
browser_pid = self._start_signed_in()
61+
# Set the intent directly: it normally derives from a locked pref, which
62+
# Marionette can't toggle the way the policy engine (which unlocks) does.
63+
self._child_driver.set_context("chrome")
64+
self._child_driver.execute_script(
65+
"Services.felt.setRestartLockIntent(arguments[0]);",
66+
script_args=(locking_enabled,),
67+
)
68+
self._child_driver.set_context("content")
69+
assert self.signout_count.value == 0, "No signout should have been posted yet"
70+
return browser_pid
71+
72+
def test_lock_on_update_restart_persists_session_without_signout(self):
73+
"""Locking enabled: an update-driven restart locks (no signout, token kept)."""
74+
browser_pid = self._begin_restart_test(locking_enabled=True)
75+
76+
self._trigger_browser_restart()
77+
self._settle_after_child_exit(browser_pid)
78+
79+
assert self.signout_count.value == 0, (
80+
f"Locking on restart must not post a signout, got {self.signout_count.value}"
81+
)
82+
assert self._felt_has_locking_token(), (
83+
"Locking on update-restart must persist an encrypted resume token"
84+
)
85+
86+
def test_update_restart_without_locking_keeps_no_token(self):
87+
"""Locking disabled: an update-driven restart neither locks nor signs out."""
88+
browser_pid = self._begin_restart_test(locking_enabled=False)
89+
90+
self._trigger_browser_restart()
91+
self._settle_after_child_exit(browser_pid)
92+
93+
assert not self._felt_has_locking_token(), (
94+
"Without locking, an update-restart must not persist a resume token"
95+
)
96+
assert self.signout_count.value == 0, (
97+
f"An update-restart must not post a signout, got {self.signout_count.value}"
98+
)

0 commit comments

Comments
 (0)