[FIX] auto_totp: totp tours when hr is not installed
When #96517 was merged, it was missed that if hr is *not* installed, then the user profile opens in a dialog in edition mode (always, can't be readonly). Since half the tours of `auth_totp` end in the profile screen (to check that the totp state is what we expect) this means they work fine in most test contexts where `hr` is installed, but they fail as soon as `hr` is *not* installed. Fix this by adding a helper function which checks whether the profile screen uses a dialog or not, and closes the dialog if so (otherwise it does nothing as the "form" profile screen is not in edition mode). While at it, improve a bunch of steps: - fold check steps which were really `extra_triggers` (something we wanted to check but not manipulate, in the same screen as something we do want to manipulate) - convert a few promise-based functions to `async` (tours don't support promises but async functions work either way and lead to simpler code here) - make better use of the tour action helpers (no need for explicit `_get_action_values` calls for the most part, and no need to call the internal versions either) - clarify a pair of fixmes as I'd completely forgotten what they meant closes odoo/odoo#97567 Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
@@ -34,6 +34,34 @@ function openUserProfileAtSecurityTab() {
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the TOTP button is in the specified state (true = enabled =
|
||||
* can disable, false = disabled = can enable), then closes the profile dialog
|
||||
* if it's one (= hr not installed).
|
||||
*
|
||||
* If no totp state is provided, just checks that the toggle exists.
|
||||
*/
|
||||
function closeProfileDialog({content, totp_state}) {
|
||||
let trigger;
|
||||
switch (totp_state) {
|
||||
case true: trigger = 'button[name=action_totp_disable]'; break;
|
||||
case false: trigger = 'button[name=action_totp_enable_wizard]'; break;
|
||||
case undefined: trigger = 'button.o_auth_2fa_btn'; break;
|
||||
default: throw new Error(`Invalid totp state ${totp_state}`)
|
||||
}
|
||||
|
||||
return [{
|
||||
content,
|
||||
trigger,
|
||||
run() {
|
||||
const $modal = this.$anchor.parents('.o_dialog_container');
|
||||
if ($modal.length) {
|
||||
$modal.find('button[name=preference_cancel]').click()
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
tour.register('totp_tour_setup', {
|
||||
test: true,
|
||||
url: '/web'
|
||||
@@ -41,35 +69,30 @@ tour.register('totp_tour_setup', {
|
||||
content: "Open totp wizard",
|
||||
trigger: 'button[name=action_totp_enable_wizard]',
|
||||
}, {
|
||||
content: "Check that we have to enter enhanced security mode",
|
||||
trigger: 'div:contains("enter your password")',
|
||||
run: () => {},
|
||||
}, {
|
||||
content: "Input password",
|
||||
content: "Check that we have to enter enhanced security mode and input password",
|
||||
extra_trigger: 'div:contains("enter your password")',
|
||||
trigger: '[name=password] input',
|
||||
run: 'text demo', // FIXME: better way to do this?
|
||||
run: 'text demo',
|
||||
}, {
|
||||
content: "Confirm",
|
||||
trigger: "button:contains(Confirm Password)",
|
||||
}, {
|
||||
content: "Check the wizard has opened",
|
||||
trigger: 'li:contains("When requested to do so")',
|
||||
run: () => {}
|
||||
run() {}
|
||||
}, {
|
||||
content: "Get secret from collapsed div",
|
||||
trigger: 'a:contains("Cannot scan it?")',
|
||||
run(helpers) {
|
||||
async run(helpers) {
|
||||
const $secret = this.$anchor.closest('div').find('[name=secret] > span');
|
||||
const $copyBtn = $secret.find('button');
|
||||
$copyBtn.remove();
|
||||
const secret = $secret.text();
|
||||
ajax.jsonRpc('/totphook', 'call', {
|
||||
secret
|
||||
}).then((token) => {
|
||||
helpers._text(helpers._get_action_values('[name=code] input'), token);
|
||||
helpers._click(helpers._get_action_values('button.btn-primary:contains(Activate)'));
|
||||
$('body').addClass('got-token')
|
||||
const token = await ajax.jsonRpc('/totphook', 'call', {
|
||||
secret: $secret.text()
|
||||
});
|
||||
helpers.text(token, '[name=code] input');
|
||||
helpers.click('button.btn-primary:contains(Activate)');
|
||||
$('body').addClass('got-token')
|
||||
}
|
||||
}, {
|
||||
content: 'wait for rpc',
|
||||
@@ -78,11 +101,11 @@ tour.register('totp_tour_setup', {
|
||||
},
|
||||
...openRoot(),
|
||||
...openUserProfileAtSecurityTab(),
|
||||
{
|
||||
...closeProfileDialog({
|
||||
content: "Check that the button has changed",
|
||||
trigger: 'button[name=action_totp_disable]',
|
||||
run: () => {}
|
||||
}]);
|
||||
totp_state: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
tour.register('totp_login_enabled', {
|
||||
test: true,
|
||||
@@ -107,18 +130,21 @@ tour.register('totp_login_enabled', {
|
||||
}, {
|
||||
content: "input code",
|
||||
trigger: 'input[name=totp_token]',
|
||||
run(helpers) {
|
||||
ajax.jsonRpc('/totphook', 'call', {}).then((token) => {
|
||||
helpers._text(helpers._get_action_values(), token);
|
||||
// FIXME: is there a way to put the button as its own step trigger without
|
||||
// the tour straight blowing through and not waiting for this?
|
||||
helpers._click(helpers._get_action_values('button:contains("Login")'));
|
||||
});
|
||||
async run(helpers) {
|
||||
// TODO: if tours are ever async-aware the click should get moved out,
|
||||
// but currently there's no great way to make the tour wait until
|
||||
// we've retrieved and set the token: `:empty()` is aboutthe text
|
||||
// content of the HTML element, not the JS value property. We
|
||||
// could set a class but that's really no better than
|
||||
// procedurally clicking the button after we've set the input.
|
||||
const token = await ajax.jsonRpc('/totphook', 'call', {});
|
||||
helpers.text(token);
|
||||
helpers.click('button:contains("Login")');
|
||||
}
|
||||
}, {
|
||||
content: "check we're logged in",
|
||||
trigger: ".o_user_menu .oe_topbar_name",
|
||||
run: () => {}
|
||||
run() {}
|
||||
}]);
|
||||
|
||||
tour.register('totp_login_device', {
|
||||
@@ -147,13 +173,10 @@ tour.register('totp_login_device', {
|
||||
}, {
|
||||
content: "input code",
|
||||
trigger: 'input[name=totp_token]',
|
||||
run(helpers) {
|
||||
ajax.jsonRpc('/totphook', 'call', {}).then((token) => {
|
||||
helpers._text(helpers._get_action_values(), token);
|
||||
// FIXME: is there a way to put the button as its own step trigger without
|
||||
// the tour straight blowing through and not waiting for this?
|
||||
helpers._click(helpers._get_action_values('button:contains("Login")'));
|
||||
});
|
||||
async run(helpers) {
|
||||
const token = await ajax.jsonRpc('/totphook', 'call', {})
|
||||
helpers.text(token);
|
||||
helpers.click('button:contains("Login")');
|
||||
}
|
||||
}, {
|
||||
content: "check we're logged in",
|
||||
@@ -179,7 +202,7 @@ tour.register('totp_login_device', {
|
||||
}, {
|
||||
content: "check we're logged in without 2FA",
|
||||
trigger: ".o_user_menu .oe_topbar_name",
|
||||
run: () => {}
|
||||
run() {}
|
||||
},
|
||||
// now go and disable two-factor authentication would be annoying to do in a separate tour
|
||||
// because we'd need to login & totp again as HttpCase.authenticate can't
|
||||
@@ -189,24 +212,21 @@ tour.register('totp_login_device', {
|
||||
content: "Open totp wizard",
|
||||
trigger: 'button[name=action_totp_disable]',
|
||||
}, {
|
||||
content: "Check that we have to enter enhanced security mode",
|
||||
trigger: 'div:contains("enter your password")',
|
||||
run: () => {},
|
||||
}, {
|
||||
content: "Input password",
|
||||
content: "Check that we have to enter enhanced security mode and input password",
|
||||
extra_trigger: 'div:contains("enter your password")',
|
||||
trigger: '[name=password] input',
|
||||
run: 'text demo', // FIXME: better way to do this?
|
||||
run: 'text demo',
|
||||
}, {
|
||||
content: "Confirm",
|
||||
trigger: "button:contains(Confirm Password)",
|
||||
},
|
||||
...openRoot(),
|
||||
...openUserProfileAtSecurityTab(),
|
||||
{
|
||||
...closeProfileDialog({
|
||||
content: "Check that the button has changed",
|
||||
trigger: 'button[name=action_totp_enable_wizard]',
|
||||
run: () => {}
|
||||
}]);
|
||||
totp_state: false
|
||||
}),
|
||||
]);
|
||||
|
||||
tour.register('totp_login_disabled', {
|
||||
test: true,
|
||||
@@ -231,6 +251,8 @@ tour.register('totp_login_disabled', {
|
||||
// issues, so go and open the preferences / profile screen to make sure
|
||||
// everything settles down
|
||||
...openUserProfileAtSecurityTab(),
|
||||
// close the dialog if that makes sense
|
||||
...closeProfileDialog({})
|
||||
]);
|
||||
|
||||
const columns = {};
|
||||
@@ -243,14 +265,14 @@ tour.register('totp_admin_disables', {
|
||||
}, {
|
||||
content: 'Wait for page',
|
||||
trigger: '.o_menu_brand:contains("Settings")',
|
||||
run: () => {}
|
||||
run() {}
|
||||
}, {
|
||||
content: "Open Users menu",
|
||||
trigger: '[data-menu-xmlid="base.menu_users"]'
|
||||
}, {
|
||||
content: "Open Users view",
|
||||
trigger: '[data-menu-xmlid="base.menu_action_res_users"]',
|
||||
run: function (helpers) {
|
||||
run(helpers) {
|
||||
// funny story: the users view we're trying to reach, sometimes we're
|
||||
// already there, but if we re-click the next step executes before the
|
||||
// action has the time to re-load, the one after that doesn't, and our
|
||||
@@ -269,7 +291,7 @@ tour.register('totp_admin_disables', {
|
||||
}, {
|
||||
content: "Find Demo User",
|
||||
trigger: 'td.o_data_cell:contains("demo")',
|
||||
run: function (helpers) {
|
||||
run(helpers) {
|
||||
const $titles = this.$anchor.closest('table').find('tr:first th');
|
||||
for (let i=0; i<$titles.length; ++i) {
|
||||
columns[$titles[i].getAttribute('data-name')] = i;
|
||||
@@ -285,13 +307,10 @@ tour.register('totp_admin_disables', {
|
||||
content: "Select totp remover",
|
||||
trigger: 'span.dropdown-item:contains(Disable two-factor authentication)'
|
||||
}, { // enhanced security yo
|
||||
content: "Check that we have to enter enhanced security mode",
|
||||
trigger: 'div:contains("enter your password")',
|
||||
run: () => {},
|
||||
}, {
|
||||
content: "Input password",
|
||||
content: "Check that we have to enter enhanced security mode & input password",
|
||||
extra_trigger: 'div:contains("enter your password")',
|
||||
trigger: '[name=password] input',
|
||||
run: 'text admin', // FIXME: better way to do this?
|
||||
run: 'text admin',
|
||||
}, {
|
||||
content: "Confirm",
|
||||
trigger: "button:contains(Confirm Password)",
|
||||
@@ -301,8 +320,9 @@ trigger: 'div:contains("enter your password")',
|
||||
}, {
|
||||
content: "go to Account security Tab",
|
||||
trigger: "a.nav-link:contains(Account Security)",
|
||||
}, {
|
||||
}, ...closeProfileDialog({
|
||||
content: "check that demo user has been de-totp'd",
|
||||
trigger: "button[name=action_totp_enable_wizard]",
|
||||
}])
|
||||
totp_state: false,
|
||||
}),
|
||||
])
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user