Remove Cypress based UI tests (#1035)

This commit is contained in:
Ximon Eighteen
2023-04-20 13:28:48 +02:00
committed by GitHub
parent 2d0814125e
commit b22beb3fa3
33 changed files with 10 additions and 3651 deletions
+1 -8
View File
@@ -50,7 +50,7 @@ jobs:
# Test with no features, default features ("") and all except UI tests.
# Order: fewest features to most features.
args: ["--no-default-features", "", "--features all-except-ui-tests"]
args: ["--no-default-features", "", "--features all"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
@@ -60,13 +60,6 @@ jobs:
rust-version: ${{ matrix.rust }}
- run: cargo build --verbose ${{ matrix.args }} --locked
- run: cargo test --verbose ${{ matrix.args }} -- --test-threads=1 2>&1
- name: Archive Cypress UI test image & video captures
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: cypress-ui-test-captures ${{ matrix.os }} ${{ matrix.rust }}
path: target/ui/
if-no-files-found: ignore
pykmip-test:
name: pykmip-test
+1 -4
View File
@@ -28,10 +28,7 @@ jobs:
with:
rust-version: ${{ matrix.rust }}
- run: cargo install cargo-tarpaulin
# use a long timeout with tarpaulin as UI tests have to docker pull the
# cypress.io image which can cause a test timeout with the default tarpaulin
# timeout of 1 minute.
- run: cargo tarpaulin --locked --verbose --out Html --timeout 900 ${{ matrix.args }}
- run: cargo tarpaulin --locked --verbose --out Html ${{ matrix.args }}
- name: Archive code coverage results
uses: actions/upload-artifact@v2
with:
Generated
-49
View File
@@ -50,12 +50,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "ascii"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
[[package]]
name = "ascii-canvas"
version = "3.0.0"
@@ -214,12 +208,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.2.5"
@@ -336,16 +324,6 @@ dependencies = [
"target-lexicon",
]
[[package]]
name = "ctrlc"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865"
dependencies = [
"nix",
"winapi",
]
[[package]]
name = "cxx"
version = "1.0.79"
@@ -975,7 +953,6 @@ dependencies = [
"chrono",
"clap",
"cryptoki",
"ctrlc",
"fern",
"fslock",
"futures",
@@ -1003,7 +980,6 @@ dependencies = [
"serde",
"serde_json",
"syslog",
"tiny_http",
"tokio",
"tokio-rustls",
"toml",
@@ -1203,17 +1179,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
@@ -2168,20 +2133,6 @@ dependencies = [
"crunchy",
]
[[package]]
name = "tiny_http"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39"
dependencies = [
"ascii",
"chrono",
"chunked_transfer",
"log",
"openssl",
"url",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
+2 -6
View File
@@ -74,16 +74,15 @@ syslog = "^4.0"
[features]
default = [ "multi-user", "hsm" ]
hsm = ["backoff", "kmip", "once_cell", "cryptoki", "r2d2"]
multi-user = [ "basic-cookies", "jmespatch/sync", "regex", "oso", "openidconnect", "rpassword", "scrypt", "unicode-normalization", "urlparse" ]
static-openssl = [ "openssl/vendored" ]
# Preview features - not ready for production use
rta = []
hsm = ["backoff", "kmip", "once_cell", "cryptoki", "r2d2"]
# Internal features - not for external use
all-except-ui-tests = [ "multi-user", "rta", "static-openssl" ]
ui-tests = []
all = [ "multi-user", "rta", "static-openssl" ]
hsm-tests-kmip = [ "hsm" ]
hsm-tests-pkcs11 = [ "hsm" ]
@@ -95,9 +94,6 @@ panic = "abort"
[dev-dependencies]
regex = "1.5.5"
urlparse = "^0.7"
# For user management
ctrlc = "^3.1"
tiny_http = { version = "^0.8", features = ["ssl"] }
# ------------------------------------------------------------------------------
# START DEBIAN PACKAGING
+4 -4
View File
@@ -506,10 +506,10 @@ In no particular order:
#### Testing
Testing the provider code in isolation cannot ensure that the chain of communication from Lagosta
via Krill to the OP and back again works as expected and yields an acceptable end user experience. Therefore the
majority of the tests use Cypress to drive Lagosta in a browser connected to an instance of Krill which in turn connects
to a locally deployed mock OP.
Testing of the UI interaction is handled by dedicated tests in the UI repository. Further tests are implemented
inline in the Oso rule files, with a few explicit Oso tests also invoked by Krill immediately after Oso is
initialized and rule files have been loaded.
#### Flow
This implementation supports the [OpenID Connect Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)
-2
View File
@@ -24,8 +24,6 @@ The feature adds several related but distinct capabilities to Krill:
- (De)Serialization
- (En/De)cryption _(powered by [rust-openssl](https://crates.io/crates/openssl))_
- Automated browser based testing _(powered by [Cypress](https://www.cypress.io/))_
- Password hashing support in `krillc`
- Propagation of the current identity all the way down to the event history
+2 -45
View File
@@ -7,51 +7,8 @@ At the lowest level, any test that exercises Krill from the outside will exercis
external and internal actions require an `Actor` instance, and all external requests must pass authentication and
authorization checks.
However, not all tests will exercise login as that is only used by Lagosta. The existing Krill integration tests use a
However, not all tests will exercise login as that is only used by the UI. The existing Krill integration tests use a
fixed admin API token without login because that was all Krill supported before, and must continue to work for the
direct REST API clients and indirect REST API client via `krillc`.
Also, as the multi-user feature is aimed entirely (at present) at Lagosta users, it only works if it works from the UI
all the way down to the event log, testing at the Krill internal library boundary or the Krill REST API boundary is
not sufficient.
As such most of the multi-user tests use [Cypress](https://cypress.io) to drive a headless browser connecting to a
locally launched instance of Krill, and for the OpenID Connect tests a homebrewed mock Rust OpenID Connect provider is
run in-process alongside Krill as well.
For ease Cypress is invoked via Docker as it has an official Docker image which contains everything needed. However,
this has not yet been verified as working on Mac OS. Cypress in turn is driven by test suites defined in Javascript.
As UI based tests can be quite slow they are gated behind their own `ui-tests` feature, which also has the benefit
that users without a working Docker setup are still able to run `cargo test`.
To run the UI tests one must therefore do:
```
cargo test --features ui-tests
```
Cypress has a very useful interactive test run mode which can be launched like so:
```
$ xhost +
$ CYPRESS_INTERACTIVE=1 cargo test --features ui-tests <some_test_name>
```
You want the `<some_test_name>` because you want Krill to be setup correctly to run a particular test that you will then run interactively, you don't want Krill to run all tests and constantly be changing the backend state as a result while you try to use Cypress to run a single test suite that has expectations about the state that Krill is in.
For example you might do:
```
$ xhost +
$ CYPRESS_INTERACTIVE=1 cargo test --features ui-tests multi_user_config_file_with_ta
```
After a short delay a browser window should open with a Cypress welcome message something like this:
![Cypress welcome popup](images/cypress-welcome-popup.png)
Dismiss the message and then click on the `multi_user_config_file_with_ta.js` test in the tree of tests that is shown to you, i.e. run the same test as you invoked with `cargo test` so that Krill has the expected configuration and starting conditions.
You should then see something like this:
![Cypress test running](images/cypress-test-running.png)
Testing of the UI specific aspects of multi-user support is handled by dedicated tests in the UI repository.
-17
View File
File diff suppressed because one or more lines are too long
-294
View File
@@ -1,294 +0,0 @@
import logging
import pytest
import rtrlib
from retrying import retry, RetryError
from time import time
from operator import attrgetter
import krill_ca_api as krill_ca_api_lib
import krill_pub_api as krill_pub_api_lib
from tests.util import krill
from tests.util.docker import docker_project, class_service_manager, function_service_manager, run_command, docker_host_fqdn
from tests.util.krill import krill_api_config
from tests.util.rtr import rtr_fetch_one, roa_to_roa_string
from tests.util.relyingparties import *
from data import *
# Test classes that use this fixture will cause Krill and its dependencies to
# be started (and torn down at the end of all tests in the class), and to be
# configured with CAs and ROas. If you want many test classes to use the
# created resources before they are torn down module scope might then be more
# appropriate.
@pytest.fixture(scope="class")
def krill_with_roas(docker_project, krill_api_config, class_service_manager):
#
# Define some retry helpers for situations where the API call to Krill
# can succeed but Krill may not yet be in the expected state.
#
def no_retry_if_forbidden(e):
"""Return True if we should retry, False otherwise"""
return not (isinstance(e, krill_ca_api_lib.ApiException) and e.status == 403)
def retry_if_not(result):
"""Return True if we should retry, False otherwise"""
# e.g. return True for empty lists, empty strings, None
return not bool(result)
@retry(
stop_max_attempt_number=10,
wait_fixed=2000,
retry_on_exception=no_retry_if_forbidden,
wrap_exception=True)
def wait_until_ready():
return krill_other_api.is_authorized()
@retry(
stop_max_attempt_number=10,
wait_exponential_multiplier=1000,
wait_exponential_max=10000,
retry_on_result=retry_if_not,
wrap_exception=True)
def wait_until_ca_has(ca_handle, property, matcher_func):
ca = krill_ca_api.get_ca(ca_handle)
f = attrgetter(property)
cas = f(ca)
return [ca for ca in cas if matcher_func(ca)]
@retry(
stop_max_attempt_number=10,
wait_exponential_multiplier=1000,
wait_exponential_max=10000,
retry_on_result=retry_if_not,
wrap_exception=True)
def wait_until_child_ca_has_at_least_one(parent_handle, child_handle, property):
ca = krill_ca_api.get_child_ca(parent_handle, child_handle)
f = attrgetter(property)
return f(ca)
@retry(
stop_max_attempt_number=10,
wait_exponential_multiplier=1000,
wait_exponential_max=10000,
retry_on_result=retry_if_not,
wrap_exception=True)
def wait_until_ca_has_resources(ca_handle, asn, ipv4, ipv6):
ca = krill_ca_api.get_ca(ca_handle)
f = attrgetter("resources")
res = f(ca)
return set(res.asn) == set(asn) and set(res.ipv4) == set(ipv4) and set(res.ipv6) == set(ipv6)
#
# define some helper functions
#
def add_ca(ca_handle):
logging.info(f'-> Adding CA "{ca_handle}"')
krill_ca_api.add_ca(krill_ca_api_lib.AddCARequest(ca_handle))
logging.info(f'-> Getting RFC 8183 publisher request for CA "{ca_handle}" (API call `get_ca_publisher_request()`)')
rfc8183_request = krill_ca_api.get_ca_publisher_request(ca_handle, format='json')
logging.info(f'-> Submitting RFC 8183 publisher request for CA "{ca_handle}" in exchange for an RFC 8183 repository_response (API call `add_publisher()`)')
rfc8183_response = krill_pub_api.add_publisher(rfc8183_request)
logging.info(f'-> Submitting RFC 8181 repository response for CA "{ca_handle}" (API call `update_ca_repository()`)')
krill_ca_api.update_ca_repository(
ca_handle,
inline_object=krill_ca_api_lib.InlineObject(repository_response=rfc8183_response))
logging.info(f'-> Added CA "{ca_handle}"')
def link_child_ca_under_parent_ca(child_ca_handle, parent_ca_handle, resources):
logging.info(f'-> Getting RFC 8183 child request for CA "{child_ca_handle}" (API call `get_ca_child_request()`)')
rfc8183_request = krill_ca_api.get_ca_child_request(child_ca_handle, format="json")
logging.info(f'-> Adding CA "{child_ca_handle}" as a child of "{parent_ca_handle}" (API call `add_child_ca()`)')
req = krill_ca_api_lib.AddCAChildRequest(
handle=child_ca_handle,
resources=resources,
id_cert=rfc8183_request.id_cert)
krill_ca_api.add_child_ca(parent_ca_handle, req)
logging.info(f'-> Added CA "{child_ca_handle} as a child of "{parent_ca_handle}"')
logging.info(f'-> Waiting for CA "{child_ca_handle}" to be registered as a child of "{parent_ca_handle}"')
wait_until_ca_has(parent_ca_handle, 'children', lambda handle: handle == child_ca_handle)
logging.info(f'-> Waiting for resources of child CA "{child_ca_handle}" to be registered')
wait_until_child_ca_has_at_least_one(parent_ca_handle, child_ca_handle, 'entitled_resources.asn')
def link_parent_ca_above_child_ca(parent_ca_handle, child_ca_handle, resources):
logging.info(f'-> Getting RFC 8183 parent response for CA "{child_ca_handle}" (API call `get_child_ca_parent_contact()`)')
rfc8183parentresponse = krill_ca_api.get_child_ca_parent_contact(parent_ca_handle, child_ca_handle)
logging.info(f'-> Adding CA "{parent_ca_handle}" as a parent of "{child_ca_handle}" (API call `add_ca_parent()`)')
req = krill_ca_api_lib.AddParentCARequest(
handle=parent_ca_handle,
contact=rfc8183parentresponse)
krill_ca_api.add_ca_parent(child_ca_handle, req)
logging.info(f'-> Added CA "{parent_ca_handle}" as a parent of "{child_ca_handle}"')
logging.info(f'-> Waiting for CA "{parent_ca_handle}" to be registered as a parent of "{child_ca_handle}"')
wait_until_ca_has(child_ca_handle, 'parents', lambda ca: ca.handle == parent_ca_handle)
logging.info(f'-> Waiting for resources of CA "{child_ca_handle}" to be issued:')
wait_until_ca_has_resources(child_ca_handle, resources.asn, resources.ipv4, resources.ipv6)
#
# Go!
#
try:
# Bring up Krill and its dependencies
krill.select_krill_config_file(docker_project, 'krill.conf')
class_service_manager.start_services_with_dependencies(docker_project, ['krill'])
# Strategy: Test then add, don't add then handle failure because that will
# cause errors to appear in the Krill server log which can be confusing
# when investigating problems.
# Get the API helper objects we need
krill_ca_api_client = krill_ca_api_lib.ApiClient(krill_api_config)
krill_ca_api = krill_ca_api_lib.CertificateAuthoritiesApi(krill_ca_api_client)
krill_roa_api = krill_ca_api_lib.RouteAuthorizationsApi(krill_ca_api_client)
krill_other_api = krill_ca_api_lib.OtherApi(krill_ca_api_client)
krill_pub_api_client = krill_pub_api_lib.ApiClient(krill_api_config)
krill_pub_api = krill_pub_api_lib.PublishersApi(krill_pub_api_client)
# Define the CA handles that we will work with
ta_handle = 'ta'
parent_handle = 'parent'
child_handle = 'child'
# Ensure that Krill is ready for our attempts to communicate with it
logging.info('Wait till we can connect to Krill...')
wait_until_ready()
#
# Create the desired state inside Krill
#
parent_resources = krill_ca_api_lib.Resources(asn=KRILL_PARENT_ASNS, ipv4=KRILL_PARENT_IPV4S, ipv6=KRILL_PARENT_IPV6S)
child_resources = krill_ca_api_lib.Resources(asn=KRILL_CHILD_ASNS, ipv4=KRILL_CHILD_IPV4S, ipv6=KRILL_CHILD_IPV6S)
logging.info(f'Checking if Krill has an embedded TA "{ta_handle}"')
ca_handles = [ca.handle for ca in krill_ca_api.list_cas().cas]
if ta_handle in ca_handles:
logging.info(f'Configuring Krill for use with embedded TA "{ta_handle}"')
logging.info(f'Adding CA "{parent_handle}" if not already present')
if not parent_handle in ca_handles:
add_ca(parent_handle)
logging.info(f'Creating TA "{ta_handle}" -> CA "{parent_handle}" relationship if not already present')
ta_children = krill_ca_api.get_ca(ta_handle).children
if not parent_handle in ta_children:
link_child_ca_under_parent_ca(parent_handle, ta_handle, parent_resources)
logging.info(f'Creating TA "{ta_handle}" <- CA "{parent_handle}" relationship if not already present')
if len(krill_ca_api.get_ca(parent_handle).parents) == 0:
link_parent_ca_above_child_ca(ta_handle, parent_handle, parent_resources)
logging.info(f'Adding CA "{child_handle}" if not already present')
if not child_handle in ca_handles:
add_ca(child_handle)
logging.info(f'Creating CA "{parent_handle}" -> CA "{child_handle}" relationship if not already present')
if len(krill_ca_api.get_ca(parent_handle).children) == 0:
link_child_ca_under_parent_ca(child_handle, parent_handle, child_resources)
logging.info(f'Creating CA "{parent_handle}" <- CA "{child_handle}" relationship if not already present')
if len(krill_ca_api.get_ca(child_handle).parents) == 0:
link_parent_ca_above_child_ca(parent_handle, child_handle, child_resources)
logging.info(f'Creating CA "{child_handle}" ROAs if not already present')
if len(krill_roa_api.list_route_authorizations(child_handle)) == 0:
delta = krill_ca_api_lib.ROADelta(added=TEST_ROAS, removed=[])
@retry(
stop_max_attempt_number=10,
wait_exponential_multiplier=1000,
wait_exponential_max=10000,
wrap_exception=True)
def update_roas():
logging.info('Updating ROAs...')
krill_roa_api.update_route_authorizations(child_handle, delta)
update_roas()
logging.info('Krill configuration complete')
except RetryError as e:
if e.last_attempt.has_exception:
(ex_type, ex_value, traceback) = e.last_attempt.value
pytest.fail(f'Retries exhausted while configuring Krill: {ex_value} caused by {e}')
else:
pytest.fail(f'Retries exhausted while configuring Krill: {e}')
yield (krill_ca_api_client, krill_pub_api_client)
@pytest.mark.usefixtures("krill_with_roas")
class TestKrillWithRelyingParties:
def test_setup(self):
# Cause the krill_with_roas and dependent fixtures to be setup once
# before the tests below run, otherwise the first real test also
# includes the work and output of creating the fixtures.
pass
#@pytest.mark.parametrize("service", [Routinator, RoutinatorUnstable, FortValidator, OctoRPKI, Rcynic, RPKIClient, RPKIValidator3])
@pytest.mark.parametrize("service", [Routinator, RoutinatorUnstable, FortValidator, OctoRPKI, Rcynic, RPKIClient])
def test_rtr(self, docker_host_fqdn, docker_project, function_service_manager, service, metadata):
#
# Use Docker Compose to deploy the given Relying Party service and its dependencies.
# On tear down the service container and its dependent containers will be killed and removed.
#
function_service_manager.start_services_with_dependencies(docker_project, service.name)
class UpdateWasEmpty(Exception):
pass
def retry_if_incomplete_update(exception):
return isinstance(exception, rtrlib.exceptions.SyncTimeout) or \
isinstance(exception, UpdateWasEmpty)
@retry(
stop_max_attempt_number=10,
wait_exponential_multiplier=5000,
wait_exponential_max=20000,
retry_on_exception=retry_if_incomplete_update,
wrap_exception=True)
def fetch_from_rtr_server():
try:
rtr_start_time = int(time())
logging.info(f'Connecting RTR client to {docker_host_fqdn}:{service.rtr_port}')
received_roas = set(rtr_fetch_one(docker_host_fqdn, service.rtr_port, service.rtr_timeout_seconds))
rtr_elapsed_time = int(time()) - rtr_start_time
# r is now a list of PFXRecord
# see: https://python-rtrlib.readthedocs.io/en/latest/api.html#rtrlib.records.PFXRecord
logging.info(f'Received {len(received_roas)} ROAs via RTR from {service.name} in {rtr_elapsed_time} seconds')
if len(received_roas) == 0:
# retry, maybe the ROAs are not available yet
raise UpdateWasEmpty()
# are each of the TEST_ROAS items in r?
# i.e. is the intersection of the two sets equal to that of the TEST_ROAS set?
logging.info(f'Comparing {len(received_roas)} received ROAs to {len(TEST_ROAS)} expected ROAs...')
expected_roas = set([roa_to_roa_string(r) for r in TEST_ROAS])
assert received_roas == expected_roas
except rtrlib.exceptions.SyncTimeout as e:
logging.error(f'Timeout (>{service.rtr_timeout_seconds} seconds) while syncing RTR with {service.name} at {docker_host_fqdn}:{service.rtr_port}')
try:
if not service.is_ready():
logging.error(f'{service.name} is not ready')
except Exception as innerE:
logging.error(f'Unable to determine if {service.name} is ready: {innerE}')
raise e
fetch_from_rtr_server()
-8
View File
@@ -1,8 +0,0 @@
#[cfg(feature = "ui-tests")]
mod ui;
#[tokio::test]
#[cfg(feature = "ui-tests")]
async fn multi_user_admin_token_test() {
ui::run_krill_ui_test("multi_user_admin_token", ui::OpenIDConnectMockConfig::do_not_start()).await;
}
-8
View File
@@ -1,8 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_config_file_test() {
ui::run_krill_ui_test("multi_user_config_file", ui::OpenIDConnectMockConfig::do_not_start()).await
}
-72
View File
@@ -1,72 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_config_file_with_ta_test() {
use log::info;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use rpki::ca::idexchange::Handle;
use krill::{
cli::{
options::{CaCommand, Command, HistoryOptions},
report::ApiResponse,
},
test::*,
};
ui::run_krill_ui_test(
"multi_user_config_file_with_ta",
ui::OpenIDConnectMockConfig::do_not_start(),
)
.await;
// Check the Krill event history after the actions performed against Krill
// by the Cypress browser driving test script we just executed. Expect at
// least one action to be attributed to the logged in user who interacted
// with the CA allocated to them and at least one action with the same CA to
// be attributed to the internal 'krill' user.
// TODO: improve this to match the exact sequence of expected actions and
// attributed actors.
info!("Verifying that CAs were modified by the expected users according to the history log");
let mut cas_and_users = HashMap::new();
cas_and_users.insert("ca_admin", vec!["krill", "user:admin@krill"]);
cas_and_users.insert(
"ca_readwrite",
vec!["krill", "user:readwrite@krill", "user:joe", "user:sally"],
);
cas_and_users.insert("ca_readonly", vec!["krill", "user:rohelper@krill"]);
for (ca, expected_users) in cas_and_users {
let r = krill_admin(Command::CertAuth(CaCommand::ShowHistoryCommands(
Handle::from_str(ca).unwrap(),
HistoryOptions::default(),
)))
.await;
assert!(
matches!(r, ApiResponse::CertAuthHistory(_)),
"Expected a history API response"
);
if let ApiResponse::CertAuthHistory(history) = r {
// each expected user should be present at least once in the history of the CA
// no other users should be present in the CA history
let expected_users_set: HashSet<String> = expected_users.iter().map(|u| u.to_string()).collect();
let found_users_set: HashSet<String> = history.commands().iter().map(|r| r.actor.clone()).collect();
assert_eq!(
expected_users_set, found_users_set,
"One or more users in the history of CA '{}' is missing or unexpected",
ca
);
}
}
}
-14
View File
@@ -1,14 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_openid_connect_test() {
use crate::ui::{OpenIDConnectMockConfig, OpenIDConnectMockMode::*};
ui::run_krill_ui_test(
"multi_user_openid_connect",
OpenIDConnectMockConfig::enabled(WithRPInitiatedLogout),
)
.await
}
@@ -1,14 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_openid_connect_provider_not_available_test() {
use crate::ui::{OpenIDConnectMockConfig, OpenIDConnectMockMode::*};
ui::run_krill_ui_test(
"multi_user_openid_connect_provider_not_available",
OpenIDConnectMockConfig::disabled(WithRPInitiatedLogout),
)
.await
}
@@ -1,14 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_openid_connect_provider_with_custom_logout() {
use crate::ui::{OpenIDConnectMockConfig, OpenIDConnectMockMode::*};
ui::run_krill_ui_test(
"multi_user_openid_connect_provider_with_custom_logout",
OpenIDConnectMockConfig::enabled(WithRPInitiatedLogout),
)
.await
}
@@ -1,14 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_openid_connect_provider_with_fallback_logout_test() {
use crate::ui::{OpenIDConnectMockConfig, OpenIDConnectMockMode::*};
ui::run_krill_ui_test(
"multi_user_openid_connect_provider_with_fallback_logout",
OpenIDConnectMockConfig::enabled(WithNoLogoutEndpoints),
)
.await
}
@@ -1,14 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_openid_connect_provider_with_revocation() {
use crate::ui::{OpenIDConnectMockConfig, OpenIDConnectMockMode::*};
ui::run_krill_ui_test(
"multi_user_openid_connect_provider_with_revocation",
OpenIDConnectMockConfig::enabled(WithOAuth2Revocation),
)
.await
}
-12
View File
@@ -1,12 +0,0 @@
#[cfg(feature = "ui-tests")]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn multi_user_team_based_access_test() {
ui::run_krill_ui_test(
"multi_user_team_based_access",
ui::OpenIDConnectMockConfig::do_not_start(),
)
.await
}
-8
View File
@@ -1,8 +0,0 @@
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
mod ui;
#[tokio::test]
#[cfg(all(feature = "ui-tests", feature = "multi-user"))]
async fn testbed_ui_test() {
ui::run_krill_ui_test("testbed_ui", ui::OpenIDConnectMockConfig::do_not_start()).await;
}
-25
View File
@@ -1,25 +0,0 @@
/* eslint-disable arrow-body-style */
// https://docs.cypress.io/guides/guides/plugins-guide.html
// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
// /* eslint-disable import/no-extraneous-dependencies, global-require */
// const webpack = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => {
// on('file:preprocessor', webpack({
// webpackOptions: require('@vue/cli-service/webpack.config'),
// watchOptions: {}
// }))
return Object.assign({}, config, {
fixturesFolder: 'test-resources',
integrationFolder: 'tests/ui/cypress/specs',
screenshotsFolder: 'target/ui/screenshots',
videosFolder: 'target/ui/videos',
supportFile: 'tests/ui/cypress/support/index.js'
})
}
@@ -1,54 +0,0 @@
let admin = { u: 'admin-token', p: 'secret' };
describe('admin API token', () => {
it('The correct login form is shown', () => {
cy.visit('/')
// make sure we haven't been redirected away from Krill (as would be the
// case if an OpenID Connect login form were shown)
cy.url().should('include', Cypress.config('baseUrl'))
// make sure that no user name field exists (as would be the case if the
// built-in config file based local user login form were shown)
cy.contains('Username').should('not.exist')
// check that a password form input field and the text Password are shown on
// the page
cy.get(':password')
cy.contains('Password')
})
it('Cannot login with empty password', () => {
cy.visit('/')
cy.get(':password').clear()
cy.contains('Sign In').click()
cy.contains('Please enter your password')
})
it('Cannot login with incorrect password', () => {
cy.visit('/')
cy.get(':password').clear().type('abc')
cy.contains('Sign In').click()
cy.contains('The credentials you specified are wrong')
})
it('Can login with correct password', () => {
cy.visit('/')
cy.get(':password').type(admin.p)
cy.contains('Sign In').click()
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
})
it('Can logout', () => {
cy.visit('/')
cy.get(':password').type(admin.p)
cy.contains('Sign In').click()
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
cy.get('.logout').click()
cy.contains('Sign In')
})
})
@@ -1,120 +0,0 @@
// A note about strong password hashing login delays
// -----------------------------------------------------------------------------
// The strong password hashing on the client and server side when logging in with
// config file users causes the login process to take a few seconds. As such we
// extend the default timeout when checking for Sign In completion, like so:
//
// cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
let admin = { u: 'admin@krill', p: 'admin_pass' };
let readonly = { u: 'readonly@krill', p: 'readonly_pass' };
let readwrite = { u: 'readwrite@krill', p: 'readwrite_pass' };
let ca_name = 'dummy-ca-name';
let login_test_settings = [
{ d: 'empty', u: '', p: '', o: false },
{ d: 'admin token', u: 'secret', p: 'secret', o: false },
{ d: 'incorrect', u: 'wrong_user_name', p: 'wrong_password', o: false },
{ d: 'admin', u: admin.u, p: admin.p, o: true },
{ d: 'readonly', u: readonly.u, p: readonly.p, o: true },
{ d: 'readwrite', u: readwrite.u, p: readwrite.p, o: true },
];
describe('Config File users', () => {
it('The correct login form is shown', () => {
cy.visit('/')
// make sure we haven't been redirected away from Krill (as would be the
// case if an OpenID Connect login form were shown)
cy.url().should('include', Cypress.config('baseUrl'))
// make sure that user name field exists (which would not be the case if the
// built-in admin token based login form were shown)
cy.contains('Username')
// check that a password form input field and the text Password are shown on
// the page
cy.get(':password')
cy.contains('Password')
})
login_test_settings.forEach(function (ts) {
it('Login with ' + ts.d + ' credentials should ' + (ts.o ? 'succeed' : 'fail'), () => {
// Work around the "Login with incorrect credentials should fail" test failing with ESOCKETTIMEDOUT by increasing
// the response timeout as mentioned on https://github.com/cypress-io/cypress/issues/7062. This isn't anything to
// do with incorrect credentials as re-ordering the tests causes a different test to fail. Rather, on a 2-vcpu
// GitHub Actions runner Azure VM Krill is apparently busy around the time of the 4th test and takes longer to
// respond. The issue is reproducible on a 1-vcpu AWS t2.small EC2 instance but not on a 2-vcpu AWS EC2 instance.
cy.visit('/', { responseTimeout: 31000 })
cy.contains('Username')
cy.contains('Password')
cy.get('input[placeholder="Your username"]').clear()
cy.get(':password').clear()
if (ts.u != '') cy.get('input[placeholder="Your username"]').type(ts.u)
if (ts.p != '') cy.get(':password').type(ts.p)
cy.contains('Sign In').click()
if (ts.u == '') cy.contains('Please enter your username')
if (ts.p == '') cy.contains('Please enter your password')
if (ts.o) {
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(ts.u)
} else {
cy.contains('Sign In')
}
})
})
it('Can logout', () => {
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(admin.u)
cy.get(':password').type(admin.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
cy.get('.logout').click()
cy.contains('Sign In')
})
it('Should be timed out', () => {
// take manual control of time in the browser
cy.clock()
// login
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(admin.u)
cy.get(':password').type(admin.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
// Skip ahead a minute and check that we are still logged in
cy.tick(1 * 60 * 1000)
cy.visit('/')
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
// Skip ahead till just before the idle timeout and check that we are still
// logged in.
cy.tick(28 * 60 * 1000)
cy.visit('/')
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
// Skip ahead another 31 minutes to just beyond the UI 30 minute idle
// timeout threshold and verify that we have been logged out
cy.tick(31 * 60 * 1000)
cy.visit('/')
cy.get('#userinfo').should('not.exist')
cy.contains('Sign In')
})
})
@@ -1,482 +0,0 @@
// This demonstrates use of config-file based users with Krill and Lagosta.
// It also demonstrates the custom role-per-ca demo policy because that
// requires multiple CAs and registered parents and repositories to demo
// ability to create ROAs but not to perform other CA "write" operations,
// which is already setup by this demo.
// A note about difficulty entering XML into Lagosta XML input fields:
// -----------------------------------------------------------------------------
// Using cy.type() to enter XML into these fields is extremely slow, one
// animated character at a time. I haven't yet found a way to copy-paste into
// them. Setting the text directly can be done but there is a challenge that has
// to be worked around which is that the fields use Prism Editor JS to syntax
// highlight the XML. Prism Editor manages the content as a rich HTML child node
// structure. Just replacing the content with a new text node doesn't work as
// the Lagosta JS code reading the field content doesn't get the content as
// set for some reason. The set text also doesn't get syntax highlighted. What
// seems to work however is causing a keyboard event in the field after the text
// has been set, e.g. pressing the End key.
//
// To summarize, the following works quickly:
// cy.get('... pre[contenteditable="true"]').invoke('text', xml)
// cy.get('... pre[contenteditable="true"]').type('{end}')
//
// This is less hacky but very slow: (even with "type(xml, {delay: 0}))")
// cy.get('... pre[contenteditable="true"]').clear().type(xml)
// A note about strong password hashing login delays
// -----------------------------------------------------------------------------
// The strong password hashing on the client and server side when logging in with
// config file users causes the login process to take a few seconds. As such we
// extend the default timeout when checking for Sign In completion, like so:
//
// cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
let admin = { u: 'admin@krill', p: 'admin' };
let readonly = { u: 'readonly@krill', p: 'readonly' };
let readwrite = { u: 'readwrite@krill', p: 'readwrite' };
let rohelper = { u: 'rohelper@krill', p: 'rohelper' };
let joe = { u: 'joe', p: 'abc' };
let sally = { u: 'sally', p: 'abc' };
// For the tests below to work these users must only have access to a single CA,
// at the time of CA creation, otherwise only the first user gets to create a CA,
// after that Lagosta doesn't prompt to create a CA as the user can already see
// that one exists.
//
// For the read only user to be able to see the repository and parent management
// UI they must be able to get past the "Welcome to Krill" screen which prompts
// to create a CA, something the read only user cannot do. Therefore the CA for
// the read only user needs to be created for it by another user, and should be
// tested for CA creation failure *before* that CA is created.
let create_ca_test_settings = [
{ d: 'readonly', u: readonly.u, p: readonly.p, o: false, ca: 'ca_readonly' },
{ d: 'readwrite', u: readwrite.u, p: readwrite.p, o: true, ca: 'ca_readwrite' },
{ d: 'admin', u: admin.u, p: admin.p, o: true, ca: 'ca_admin' },
{ d: 'rohelper', u: rohelper.u, p: rohelper.p, o: true, ca: 'ca_readonly' }, // create the CA for the readonly user as they cannot do it themselves
];
let register_publisher_test_settings = [
{ d: 'readonly', u: readonly.u, p: readonly.p, o: false, a: 'Register', ca: 'ca_readonly' },
{ d: 'readwrite', u: readwrite.u, p: readwrite.p, o: true, a: 'Register', ca: 'ca_readwrite' },
{ d: 'admin', u: admin.u, p: admin.p, o: true, a: 'Register', ca: 'ca_admin' },
{ d: 'rohelper', u: rohelper.u, p: rohelper.p, o: true, a: 'Unregister', ca: 'ca_readonly' }, // unregister the half-registered publisher created by the readonly user
{ d: 'rohelper', u: rohelper.u, p: rohelper.p, o: true, a: 'Register', ca: 'ca_readonly' }, // re-register it properly now
];
let register_parent_test_settings = [
{ d: 'readonly', u: readonly.u, p: readonly.p, o: false, a: 'Register', ca: 'ca_readonly' },
{ d: 'readwrite', u: readwrite.u, p: readwrite.p, o: true, a: 'Register', ca: 'ca_readwrite' },
{ d: 'admin', u: admin.u, p: admin.p, o: true, a: 'Register', ca: 'ca_admin' },
{ d: 'rohelper', u: rohelper.u, p: rohelper.p, o: true, a: 'Unregister', ca: 'ca_readonly' }, // unregister the half-registered parent created by the readonly user
{ d: 'rohelper', u: rohelper.u, p: rohelper.p, o: true, a: 'Register', ca: 'ca_readonly' }, // re-register it properly now
];
let add_roa_test_settings = [
{ d: 'readonly', u: readonly.u, p: readonly.p, o: false, ca: 'ca_readonly' },
{ d: 'readwrite', u: readwrite.u, p: readwrite.p, o: true, ca: 'ca_readwrite' },
{ d: 'admin', u: admin.u, p: admin.p, o: true, ca: 'ca_admin' },
];
let joe_cas = ['ta', 'testbed', 'ca_admin', 'ca_readwrite', 'ca_readonly'];
let sally_cas = ['ca_readwrite', 'ca_readonly'];
describe('Config File Users with TA', () => {
create_ca_test_settings.forEach(function (ts) {
it('Create CA as ' + ts.d + ' user should ' + (ts.o ? 'succeed' : 'fail'), () => {
// sign in
cy.visit('/')
cy.get('#login_id').type(ts.u)
cy.get('#login_password').type(ts.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(ts.u)
cy.contains('Welcome to Krill')
// create a CA
cy.contains('CA Handle')
cy.get('form input[type="text"]').type(ts.ca)
cy.contains('Create CA').click()
cy.contains('OK').click()
// no longer on the welcome page
if (ts.o) {
cy.contains('Welcome to Krill').should('not.exist')
} else {
cy.contains('Welcome to Krill')
}
})
})
register_publisher_test_settings.forEach(function (ts) {
it(ts.a + ' CA ' + ts.ca + ' with repository as ' + ts.d + ' user should ' + (ts.o ? 'succeed' : 'fail'), () => {
if (ts.a == 'Register') {
cy.intercept({ method: 'GET', path: '/api/v1/cas/' + ts.ca + '/id/publisher_request.xml'}).as('getRepoRequestXML')
// sign in
cy.visit('/')
cy.get('#login_id').type(ts.u)
cy.get('#login_password').type(ts.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(ts.u)
// wait for Lagosta to finish fetching the repository request XML
cy.wait('@getRepoRequestXML').its('response.statusCode').should('eq', 200)
// grab the repository tab publisher request XML from the Krill UI
cy.get('div#tab-repo').click()
cy.get('div#pane-repo pre[contenteditable="false"] code').contains('<publisher_request')
cy.get('div#pane-repo pre[contenteditable="false"] code').invoke('text').then(pub_req_xml => {
// use the local testbed UI to submit the request to register the publisher
cy.visit("/index.html#/testbed")
// enter the request XML into the testbed UI edit field
cy.get('div#tab-addPublisher').contains('Register Publisher').click()
cy.get('#addPublisher pre[contenteditable="true"]').invoke('text', pub_req_xml)
cy.get('#addPublisher pre[contenteditable="true"]').type('{end}')
cy.get('#addPublisher button').contains('Register publisher').click()
cy.get('div[role="dialog"] button').contains('OK').click()
cy.contains('has been added to the testbed')
// note: publisher registration succeeds even for the readonly user
// because for the testbed half of the XML exchange the readonly user is
// automatically promoted for the duration of the request to the
// internal 'testbed' user, so that the testbed is usable without
// requiring user accounts.
// grab the repository response XML from the testbed UI
cy.get('#addPublisher pre[contenteditable="false"]').contains('<repository_response')
cy.get('#addPublisher pre[contenteditable="false"]').invoke('text').then(repo_resp_xml => {
// navigate back to Krill
cy.visit("/")
// enter the response XML into the Krill UI edit field
cy.get('div#tab-repo').click()
cy.get('div#pane-repo pre[contenteditable="true"]').invoke('text', repo_resp_xml)
cy.get('div#pane-repo pre[contenteditable="true"]').type('{end}')
cy.get('div#pane-repo button').contains('Confirm').click()
if (ts.o) {
cy.contains('Success')
cy.contains('Error').should('not.exist')
} else {
cy.contains('Success').should('not.exist')
cy.contains('Error')
}
})
})
} else {
// use the local testbed UI to unregister the publisher
cy.visit("/index.html#/testbed")
// enter the registered publisher name into the testbed UI edit field
cy.get('div#tab-removePublisher').contains('Unregister Publisher').click()
cy.get('#removePublisher input[placeholder="Enter the Publisher name to remove"]').type(ts.ca)
cy.get('#removePublisher button').contains('Remove publisher').click()
cy.get('div[role="dialog"] button').contains('OK').click()
if (ts.o) {
cy.contains('has been removed')
} else {
cy.contains('has been removed').should('not.exist')
}
}
})
})
register_parent_test_settings.forEach(function (ts) {
it(ts.a + ' CA ' + ts.ca + ' with parent as ' + ts.d + ' user should ' + (ts.o ? 'succeed' : 'fail'), () => {
if (ts.a == 'Register') {
cy.intercept({ method: 'GET', path: '/api/v1/cas/' + ts.ca + '/id/child_request.xml'}).as('getChildRequestXML')
// sign in
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(ts.u)
cy.get(':password').type(ts.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(ts.u)
// wait for Lagosta to finish fetching the repository request XML
cy.wait('@getChildRequestXML').its('response.statusCode').should('eq', 200)
// grab the parents tab child request XML from the Krill UI
cy.get('div#tab-parents').click()
cy.get('div#pane-parents pre[contenteditable="false"] code').contains("<child_request")
cy.get('div#pane-parents pre[contenteditable="false"] code').invoke('text').then(child_req_xml => {
// use the local testbed UI to submit the request to register the child
cy.visit("/index.html#/testbed")
// enter the request XML into the testbed UI edit field
cy.get('div#tab-addChild').contains('Register CA').click()
cy.get('#addChild pre[contenteditable="true"]').invoke('text', child_req_xml)
cy.get('#addChild pre[contenteditable="true"]').type('{end}')
cy.get('#addChild input[placeholder^="The AS resources"]').type('AS18')
cy.get('#addChild input[placeholder^="The IPv4 resources"]').type('10.0.0.0/24')
cy.get('#addChild button').contains('Register child CA').click()
cy.get('div[role="dialog"] button').contains('OK').click()
cy.contains('has been added to the testbed')
// grab the parent response XML from the testbed UI
cy.get('#addChild pre[contenteditable="false"]').contains("<parent_response")
cy.get('#addChild pre[contenteditable="false"]').invoke('text').then(parent_resp_xml => {
// navigate back to Krill
cy.visit("/")
// enter the response XML into the Krill UI edit field
cy.get('div#tab-parents').click()
cy.get('div#pane-parents pre[contenteditable="true"]').invoke('text', parent_resp_xml)
cy.get('div#pane-parents pre[contenteditable="true"]').type('{end}')
cy.get('div#pane-parents button').contains('Confirm').click()
if (ts.o) {
cy.contains('Success')
cy.contains('Error').should('not.exist')
// wait for the parent registration to complete inside Krill and
// for the details to appear in the Lagosta UI
cy.get('div#tab-parents').click().get('body').contains('Add an additional parent')
} else {
cy.contains('Success').should('not.exist')
cy.contains('Error')
}
})
})
} else {
// use the local testbed UI to unregister the parent
cy.visit("/index.html#/testbed")
// enter the registered parent name into the testbed UI edit field
cy.get('div#tab-removeChild').contains('Unregister CA').click()
cy.get('#removeChild input[placeholder="Enter the CA name to remove"]').type(ts.ca)
cy.get('#removeChild button').contains('Remove child CA').click()
cy.get('div[role="dialog"] button').contains('OK').click()
if (ts.o) {
cy.contains('has been removed')
} else {
cy.contains('has been removed').should('not.exist')
}
}
})
})
add_roa_test_settings.forEach(function (ts) {
it('Add ROA for CA ' + ts.ca + ' as ' + ts.d + ' user should ' + (ts.o ? 'succeed' : 'fail'), () => {
cy.intercept({ method: 'GET', path: '/api/v1/cas/' + ts.ca + '/routes/analysis/full'}).as('analyzeRoutes')
// sign in
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(ts.u)
cy.get(':password').type(ts.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(ts.u)
// add a ROA
cy.get('div#tab-roas').click()
cy.get('body').then(($body) => {
// Check if Krill has issued the resources to the CA yet by seeing if the UI was able to fetch them, if it
// wasn't then it shows a "Click here to refresh" link. If the link exists, don't click it immediately as that
// will just result in the same lack of resources, instead give Krill some time (5 seconds) in this case then
// click the refresh link and then make sure that the link no longer exists (because resources were found).
// Ideally we would not wait 5 seconds but instead keep retrying until the link disappears, but according to
// Cypress docs it explicitly will NOT retry a .click() command. See:
// https://docs.cypress.io/guides/core-concepts/retry-ability.html#Why-are-some-commands-NOT-retried
// https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/
// The latter suggests to use a 3rd party cypress-pipe plugin and not to use waits. That would be nice, but to
// use a plugin we then need a custom Docker image which is something I'd rather not build, publish and maintain
// the moment. TODO: don't publish an image, instead build it on the test runner just before running the tests?
if ($body.find('#no_resources_click_to_refresh').length > 0) {
cy.get('#no_resources_click_to_refresh').wait(5000).click().get('body').get('#no_resources_click_to_refresh').should('not.exist')
}
})
// wait for Lagosta to finish fetching the route analysis details
cy.wait('@analyzeRoutes').its('response.statusCode').should('eq', 200)
cy.get('div#pane-roas button').contains('Add ROA').click()
cy.get('div[role="dialog"]')
cy.contains('Add ROA')
cy.get('#add_roa_asn').clear().type('AS18')
cy.get('#add_roa_prefix').clear().type('10.0.0.1/32')
cy.get('div[role="dialog"] button').contains('Confirm').click()
if (ts.o) {
cy.contains('ROA added')
} else {
cy.contains('Your user does not have sufficient rights to perform this action. Please contact your administrator.')
}
})
})
// This test exercises the custom role-per-ca demo policy.
// As Joe should only be able to do write operations to the CA called 'ca_readwrite', so we test:
// - Which CAs can Joe see in the CA dropdown list? Joe should be able to see them all.
// - Can Joe create a ROA on ca_readonly? This should fail.
// - Can Joe create a ROA on ca_readwrite? This should succeed.
// - Can Joe add an additional parent to ca_readwrite? This should succeed.
it('CUSTOM POLICY: Joe can see all CAs but only write to ca_readwrite', () => {
cy.intercept({ method: 'GET', path: '/api/v1/cas/ca_readonly/repo/status'}).as('statusRO')
cy.intercept({ method: 'GET', path: '/api/v1/cas/ca_readwrite/repo/status'}).as('statusRW')
cy.intercept({ method: 'GET', path: '/api/v1/cas/ca_readwrite/id/child_request.xml'}).as('getChildRequestXML')
// sign in
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(joe.u)
cy.get(':password').type(joe.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(joe.u)
// the CA drop down should contain all the CAs
// click the dropdown to open it and show the list
cy.get('.switcher > .el-select > .el-input > .el-input__inner').click()
// check that the list contains every CA
joe_cas.forEach(function (ca_name) {
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains(ca_name)
})
// ensure we are working with CA ca_readonly
cy.url().then(($url) => {
if (!$url.includes('#/cas/ca_readonly')) {
// only change the current CA and wait for an update from the backend if the current CA isn't the one we want
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains('ca_readonly').click()
cy.wait('@statusRO')
}
})
// attempting to create a ROA on ca_readonly should fail
cy.get('#tab-roas').click()
cy.contains('Add ROA').click()
cy.get('div[role="dialog"]')
cy.contains('Add ROA')
cy.get('#add_roa_asn').clear().type('AS18')
cy.get('#add_roa_prefix').clear().type('10.0.0.1/32')
cy.get('div[role="dialog"] button').contains('Confirm').click()
cy.contains('Your user does not have sufficient rights to perform this action. Please contact your administrator.')
cy.get('div[role="dialog"] button').contains('Cancel').click()
// attempting to create a ROA on ca_readwrite should succeed
cy.get('.switcher > .el-select > .el-input > .el-input__inner').click()
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains('ca_readwrite').click()
cy.wait('@statusRW')
cy.get('#tab-roas').click()
cy.contains('Add ROA').click()
cy.get('div[role="dialog"]')
cy.contains('Add ROA')
cy.get('#add_roa_asn').clear().type('AS19')
cy.get('#add_roa_prefix').clear().type('10.0.0.1/32')
cy.get('div[role="dialog"] button').contains('Confirm').click()
cy.contains('ROA added')
// attempting to add a parent on ca_readwrite should succeed
cy.get('#tab-parents').click()
cy.contains('Add an additional parent').click()
// grab the parents tab child request XML from the Krill UI
cy.wait('@getChildRequestXML').its('response.statusCode').should('eq', 200)
cy.get('div#pane-parents pre[contenteditable="false"] code').contains("<child_request")
cy.get('div#pane-parents pre[contenteditable="false"] code').invoke('text').then(child_req_xml => {
// use the local testbed UI to submit the request to register the child
cy.visit("/index.html#/testbed")
// enter the request XML into the testbed UI edit field
cy.get('div#tab-addChild').contains('Register CA').click()
cy.get('#addChild pre[contenteditable="true"]').invoke('text', child_req_xml)
cy.get('#addChild pre[contenteditable="true"]').type('{end}')
cy.get('#addChild input[placeholder^="The AS resources"]').type('AS192')
cy.get('#addChild input[placeholder^="The IPv4 resources"]').type('192.168.0.0/24')
cy.get('#addChild button').contains('Register child CA').click()
cy.get('div[role="dialog"] button').contains('OK').click()
cy.contains('has been added to the testbed')
// grab the parent response XML from the testbed UI
cy.get('#addChild pre[contenteditable="false"]').contains("<parent_response")
cy.get('#addChild pre[contenteditable="false"]').invoke('text').then(parent_resp_xml => {
// navigate back to Krill
cy.visit("/")
// enter the response XML into the Krill UI edit field
cy.get('#tab-parents').click()
cy.contains('Add an additional parent').click()
cy.get('div#pane-parents pre[contenteditable="true"]').invoke('text', parent_resp_xml)
cy.get('div#pane-parents pre[contenteditable="true"]').type('{end}')
// change the default name for the parent as there is already a parent named testbed
// TODO: this CSS selector is unreadable and unreliable, give the input field an ID
// and select that instead
cy.get('.mt-3 > .el-col > .el-input > .el-input__inner').type('otherparent')
cy.get('div#pane-parents button').contains('Confirm').click()
cy.contains('Success')
})
})
})
// This test exercises the custom role-per-ca demo policy.
// As Sally should only be able to see two CAs and only be able to add ROAs to one other CA, we test:
// - Which CAs can Sally see in the CA dropdown list? Sally should only be able to see a specific subset.
// - Can Sally create a ROA on ca_readonly? This should fail.
// - Can Sally create a ROA on ca_readwrite? This should succeed.
// - Can Sally add an additional parent to ca_readwrite? This should fail.
//
// TODO: the CA dropdown box CSS selector is unreadable and unreliable, give the custom dropdown inner input field an
// ID and select that instead. Similarly, the CA title selector is a weak selector, it can easily break later or match
// the wrong thing if the UI design is changed and should alos be given its own ID to match on.
it('CUSTOM POLICY: Sally can only see two CAs and only make ROA changes in one CA', () => {
cy.intercept({ method: 'GET', path: '/api/v1/cas/ca_readonly/repo/status'}).as('statusRO')
cy.intercept({ method: 'GET', path: '/api/v1/cas/ca_readwrite/repo/status'}).as('statusRW')
// sign in
cy.visit('/')
cy.get('input[placeholder="Your username"]').type(sally.u)
cy.get(':password').type(sally.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(sally.u)
// the CA drop down should contain all the CAs
// click the dropdown to open it and show the list
cy.get('.switcher > .el-select > .el-input > .el-input__inner').click()
// check that the list contains every CA
sally_cas.forEach(function (ca_name) {
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains(ca_name)
})
// attempting to create a ROA on ca_readonly should fail
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains('ca_readonly').click()
cy.wait('@statusRO')
cy.get('h3 > strong').contains('ca_readonly')
cy.get('#tab-roas').click()
cy.contains('Add ROA').click()
cy.get('div[role="dialog"]')
cy.contains('Add ROA')
cy.get('#add_roa_asn').clear().type('AS18')
cy.get('#add_roa_prefix').clear().type('10.0.0.1/32')
cy.get('div[role="dialog"] button').contains('Confirm').click()
cy.contains('Your user does not have sufficient rights to perform this action. Please contact your administrator.')
cy.get('div[role="dialog"] button').contains('Cancel').click()
// attempting to create a ROA on ca_readwrite should succeed
cy.get('.switcher > .el-select > .el-input > .el-input__inner').click()
cy.get('.el-select-dropdown__wrap.el-scrollbar__wrap > ul').contains('ca_readwrite').click()
cy.wait('@statusRW')
cy.get('h3 > strong').contains('ca_readwrite')
cy.get('#tab-roas').click()
cy.contains('Add ROA').click()
cy.get('div[role="dialog"]')
cy.contains('Add ROA')
cy.get('#add_roa_asn').clear().type('AS22')
cy.get('#add_roa_prefix').clear().type('10.0.0.1/32')
cy.get('div[role="dialog"] button').contains('Confirm').click()
cy.contains('ROA added')
// attempting to add an additional parent on ca_readwrite should fail
cy.get('#tab-parents').click()
cy.contains('Add an additional parent').click()
cy.contains('Confirm').click()
cy.contains('Your user does not have sufficient rights to perform this action. Please contact your administrator.')
})
})
@@ -1,424 +0,0 @@
// Matches daemon::auth::providers::openid_connect::http_client::openid_connect_provider_timeout() when test mode is
// enabled.
const KRILL_TEST_HTTP_CLIENT_TIMEOUT_SECS = 5;
// The mock OpenID Connect provider only checks usernames, not passwords.
const admin = { u: 'adm@krill' }
const readonly = { u: 'ro@krill' }
const readwrite = { u: 'rw@krill' }
const shorttoken = { u: 'shorttokenwithoutrefresh@krill' }
const shortrefresh = { u: 'shorttokenwithrefresh@krill' }
const badidtoken = { u: 'non-spec-compliant-idtoken-payload' }
const badrole = { u: 'user-with-unknown-role' }
const refreshinvalidrequest = { u: 'user-with-invalid-request-on-refresh' }
const refreshinvalidclient = { u: 'user-with-invalid-client-on-refresh' }
const wrongcsrfstate = { u: 'user-with-wrong-csrf-state-value' }
const ca_name = 'dummy-ca-name'
// d: description, u: user, o: outcome, fm: failure mode, r: role
const login_test_settings = [
{ d: 'empty', u: '', o: false },
{ d: 'incorrect', u: 'wrong_user_name', o: false, fm: 'unknown_user' },
{ d: 'admin', u: admin.u, o: true, r: 'admin' },
{ d: 'readonly', u: readonly.u, o: true, r: 'readonly' },
{ d: 'readwrite', u: readwrite.u, o: true, r: 'readwrite' },
{ d: 'badidtoken', u: badidtoken.u, o: false, fm: 'malformed_id_token' },
{ d: 'badrole', u: badrole.u, o: false },
{ d: 'wrongcsrfstate', u: wrongcsrfstate.u, o: false, fm: 'wrong_csrf_state' },
]
// o: outcome
const short_token_test_settings = [
{ ca: 'some-handle-name', o: true, token_secs: 5, create_ca_after_secs: 0 }, // should succeed with a freshly issued token
{ ca: 'some-other-handle-name', o: true, token_secs: 10, create_ca_after_secs: 5 }, // should succeed with a token due to expire but not yet expired
{ ca: 'yet-another-handle-name', o: false, token_secs: 5, create_ca_after_secs: 10 }, // should fail after token expiration
]
// fm: failure mode
const create_ca_settings_401 = [
'invalid_request',
'invalid_grant',
'invalid_client',
'http_500',
'http_503',
].map((fm) => ({
fm: fm,
responseCode: 401,
}))
// fm: failure mode
const create_ca_settings_403 = [
'unauthorized_client',
'invalid_scope',
'unsupported_grant_type',
].map((fm) => ({
fm: fm,
responseCode: 403,
}))
describe('OpenID Connect provider with RP-Initiated logout', () => {
it('The correct login form is shown', () => {
// cy.intercept({ method: 'GET', path: '/api/v1/authorized'}).as('isAuthorized')
// cy.intercept({ method: 'GET', path: '/auth/login'}).as('getLoginURL')
// cy.intercept({ method: 'GET', url: /^https:\/\/localhost:1818\/authorize.+/}).as('oidcLoginForm')
cy.visit('/')
// cy.wait(['@isAuthorized', '@getLoginURL', '@oidcLoginForm'])
// make sure we haven't been redirected away from Krill (as would be the
// case if an OpenID Connect login form were shown)
cy.url().should('not.include', Cypress.config('baseUrl'))
// make sure that this is our mock OpenID Connect provider
cy.contains('Mock OpenID Connect login form')
// check that a username input field is shown on the page
cy.get('input[name="username"]')
})
login_test_settings.forEach(function (ts) {
it(
'Login with ' +
ts.d +
' credentials should ' +
(ts.o ? 'succeed with the expected user info' : 'fail with the expected error'),
() => {
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
// Login, and while doing so specify the behaviour we want the OpenID Connect mock to exhibit for this user
if (ts.u != '') {
cy.get('input[name="username"]').clear().type(ts.u)
}
if (ts.fm) {
// Cause the mock to exhibit the requested failure mode
cy.get('select[name="failure_mode"]').select(ts.fm)
}
if (ts.r) {
// Force the mock to respond with a role attribute for this user
cy.get('input[name="userattr1"]').clear().type('role')
cy.get('input[name="userattrval1"]').clear().type(ts.r)
}
cy.contains('Sign In').click()
// We should end up back in the Krill UI
cy.url().should('include', Cypress.config('baseUrl'))
if (ts.o) {
// A good outcome, i.e. login should have succeeded
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(ts.u)
cy.get('#userinfo_table').contains(ts.r) // assumes that ts.r is not a substring of ts.u
// check the source="config-file" "extra" property claim mapping
// for the only user for which a value is defined in krill.conf: 'admin'
if (ts.d == 'admin') {
cy.get('#userinfo_table').contains('extra_val')
}
} else if (ts.d == 'badidtoken') {
cy.contains('OpenID Connect: Code exchange failed: Failed to parse server response')
cy.contains('return to the login page')
} else if (ts.d == 'badrole') {
cy.contains(
'Your user does not have sufficient rights to perform this action. Please contact your administrator.'
)
cy.contains('return to the login page')
} else if (ts.d == 'wrongcsrfstate') {
cy.contains('CSRF token mismatch')
} else {
cy.contains('The supplied login credentials were incorrect')
cy.contains('return to the login page')
}
}
)
})
it('Can logout', () => {
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(admin.u)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('admin')
cy.contains('Sign In').click()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(admin.u)
// verify that the mock provider thinks the user is logged in
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=' + admin.u, failOnStatusCode: false }).its('status').should('eq', 200)
// logout
cy.intercept({ method: 'GET', url: /^https:\/\/localhost:1818\/logout.+/}).as('oidcLogout')
cy.get('.logout').click()
cy.wait('@oidcLogout').its('response.statusCode').should('eq', 302)
// verify that the mock provider thinks the user is now logged out
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=' + admin.u, failOnStatusCode: false }).its('status').should('eq', 400)
// verify that we are shown the OpenID Connect provider login page
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]')
})
it('Login with short-lived non-refreshable token and try to refresh page', () => {
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(shorttoken.u)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readwrite')
cy.get('input[name="refresh"]').uncheck() // prevent issuing of refresh tokens for this user
cy.contains('Sign In').click()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(shorttoken.u)
// the token has a lifetime of 5 second and no refresh token
// wait 6 seconds...
// note: a shorter token with a 1 second lifetime doesn't work in the GitHub
// Action runner environment because the token has sometimes already expired
// by the time Krill verifies it!
cy.wait(6000)
// verify that if we reload the Krill UI we are shown the OpenID Connect
// provider login page
// cy.intercept({ method: 'GET', path: '/auth/login'}).as('getLoginURL')
// cy.intercept({ method: 'GET', url: /^https:\/\/localhost:1818\/authorize.+/}).as('oidcLoginForm')
cy.visit('/')
// cy.wait(['@getLoginURL', '@oidcLoginForm'])
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
})
short_token_test_settings.forEach((ts) =>
it('Login with short-lived non-refreshable token and try to create a CA after ' + ts.create_ca_after_secs + ' out of ' + ts.token_secs + ' secs' + ' should ' + (ts.o ? 'succeed' : 'fail'), () => {
// note: a short token with a 1 second lifetime doesn't work in the GitHub
// Action runner environment because the token has sometimes already expired
// by the time Krill verifies it! And we also want to test that we still have
// rights when less than half the token lifetime is remaining (as at this
// point Krill switches from considering the token to be ACTIVE to NEEDS
// REFRESH), and for a short lifetime like 5 seconds window in which to time
// the test to check after 3 seconds but before 5 seconds is just too small,
// so we use a longer lifetime for this test.
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(shorttoken.u + '_delay_' + ts.create_ca_after_secs)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readwrite')
cy.get('input[name="userattr2"]').clear().type('inc_cas') // force the create CA welcome page to show
cy.get('input[name="userattrval2"]').clear().type(ts.ca) // (by making Lagosta think there are no CAs)
cy.get('input[name="refresh"]').uncheck() // prevent issuing of refresh tokens for this user
cy.get('input[name="token_secs"]').clear().type(ts.token_secs) // control the lifetime of the issued access token
cy.contains('Sign In').click()
// record the approximate time at which the token was issued
let issued_at_ms = Date.now()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(shorttoken.u)
// verify that we are shown the CA create page
cy.contains('Welcome to Krill')
// calculate the remaining time necessary to wait until the
// create_ca_after_secs moment
let time_elapsed_ms = Date.now() - issued_at_ms
let time_remaining_ms = ts.create_ca_after_secs*1000 - time_elapsed_ms
cy.wait(time_remaining_ms)
// Try to create a CA, by typing in the input, clicking the 'Create CA' button
// and then clicking 'Ok'. This should fail, since the token can't be refreshed.
cy.intercept({ method: 'POST', path: '/api/v1/cas'}).as('createCA')
cy.contains('CA Handle')
cy.get('form input[type="text"]').type(ts.ca)
cy.contains('Create CA').click()
cy.contains('OK').click()
if (ts.o) {
cy.wait('@createCA').its('response.statusCode').should('eq', 200)
} else {
cy.wait('@createCA').its('response.statusCode').should('eq', 401)
cy.contains('Your login session has expired. Please login again.')
}
})
);
[...create_ca_settings_401, ...create_ca_settings_403].forEach((ts) =>
it('Try to create a CA with mock failure mode ' + ts.fm + ' enabled', () => {
let user_name = 'user_' + ts.fm;
let ca_name = 'some-unique-handle-name-' + Date.now();
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(user_name)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readwrite')
cy.get('input[name="userattr2"]').clear().type('inc_cas') // force the create CA welcome page to show
cy.get('input[name="userattrval2"]').clear().type(ca_name) // (by making Lagosta think there are no CAs)
cy.get('select[name="failure_mode"]').select(ts.fm)
cy.get('select[name="failure_endpoint"]').select('token')
cy.contains('Sign In').click()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(user_name)
// verify that we are shown the create CA welcome page
cy.contains('Welcome to Krill')
// the token has a lifetime of 5 second and no refresh token
// wait 6 seconds...
// note: a shorter token with a 1 second lifetime doesn't work in the GitHub
// Action runner environment because the token has sometimes already expired
// by the time Krill verifies it!
cy.wait(6000)
// Try to create a CA, by typing in the input, clicking the 'Create CA' button
// and then clicking 'Ok'. This should fail, since the mock server should return a
// exhibit the undesirable behaviour we configured which should result in an
// error from Krill.
cy.intercept({ method: 'POST', path: '/api/v1/cas'}).as('createCA')
cy.contains('CA Handle')
cy.get('form input[type="text"]').type(ca_name)
cy.contains('Create CA').click()
cy.contains('OK').click()
cy.wait('@createCA').its('response.statusCode').should('eq', ts.responseCode)
})
)
it('Login with short-lived refreshable token and try to refresh page', () => {
let token_secs = 2;
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(shortrefresh.u)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readonly')
cy.get('input[name="token_secs"]').clear().type(token_secs) // control the lifetime of the issued access token
cy.contains('Sign In').click()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(shortrefresh.u)
for (let i = 0; i < 5; i++) {
// the token has a lifetime of 2 seconds and has a refresh token
// wait 3 seconds..
// note: a shorter token with a 1 second lifetime doesn't work in the
// GitHub Action runner environment because the token has sometimes
// already expired by the time Krill verifies it!
cy.wait(1000 * (token_secs + 1))
// verify that we are still logged in to Krill
cy.visit('/')
cy.url().should('include', Cypress.config('baseUrl'))
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(shortrefresh.u)
}
});
[-2, +2].forEach((timeout_adjust_secs) =>
it('Slow provider response (' + (timeout_adjust_secs < 0 ? 'within' : 'beyond') + ' Krill HTTP client timeout) is handled correctly', () => {
let name_prefix = 'slow-response-';
let name_postfix = (timeout_adjust_secs < 0 ? 'within' : 'beyond') + '-krill-max';
let user_name = name_prefix + name_postfix;
let ca_name = name_prefix + 'ca-' + name_postfix;
let delay_secs = KRILL_TEST_HTTP_CLIENT_TIMEOUT_SECS + timeout_adjust_secs;
// Pick a token expiration time that is not too long so we don't have to wait unnecessarily, but not too short as
// on a slow system like GitHub Actions it can take a few seconds just for login to complete and then one of the
// static asset fetches from the browser to Krill causes the token refresh attempt to occur already (and we don't
// want it to occur until we try to create a CA).
let token_secs = 5;
// login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(user_name)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readwrite')
cy.get('input[name="userattr2"]').clear().type('inc_cas') // force the create CA welcome page to show
cy.get('input[name="userattrval2"]').clear().type(ca_name) // (by making Lagosta think there are no CAs)
cy.get('select[name="failure_mode"]').select('slow_response')
cy.get('select[name="failure_endpoint"]').select('token')
cy.get('input[name="failure_param"]').clear().type(delay_secs) // control the delay at the provider
cy.get('input[name="token_secs"]').clear().type(token_secs) // control the lifetime of the issued access token
cy.contains('Sign In').click()
// record the approximate time at which the token was issued
let issued_at_ms = Date.now()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(user_name)
// verify that we are shown the create CA welcome page
cy.contains('Welcome to Krill')
// wait for the access token issued to Krill to expire so that it is forced to use the provider token endpoint to
// exchange the refresh token for a new access token
let time_elapsed_ms = Date.now() - issued_at_ms
let time_till_after_expiration_ms = (token_secs * 1000) - time_elapsed_ms + 1000
cy.log('Waiting ' + time_till_after_expiration_ms + 'ms until the Krill access token ' + token_secs*1000 + 'ms expiration point should have passed')
cy.wait(time_till_after_expiration_ms)
// Try to create a CA, by typing in the input, clicking the 'Create CA' button and then clicking 'Ok'.
cy.intercept({ method: 'POST', path: '/api/v1/cas'}).as('createCA')
cy.contains('CA Handle')
cy.get('form input[type="text"]').type(ca_name)
cy.contains('Create CA').click()
cy.contains('OK').click()
// Verify that the attempt to create the CA occurred.
//
// In the case where we configure the provider to respond slowly, but still within the Krill HTTP client timeout,
// the CA creation attempt should be successful, and the response should have a new bearer token piggybacked on it
// (which resulted from the token refresh attempt).
//
// In the case where we configure the provider to take longer to respond than Krill will wait, the CA creation
// attempt should fail because Krill should have been unable to refresh its expired access token and thus should
// deny the CA creation request.
let expected_status_code = (timeout_adjust_secs < 0 ? 200 : 401);
let time_till_after_provider_delay_is_over_ms = delay_secs * 1000;
let time_to_wait_ms = time_till_after_provider_delay_is_over_ms + 3000;
if (timeout_adjust_secs < 0) {
cy.log('Expecting within ' + time_till_after_provider_delay_is_over_ms + 'ms the provider to finish delaying and for Krill to create the CA')
} else {
cy.log('Expecting Krill to timeout the provider before the ' + time_till_after_provider_delay_is_over_ms + 'ms remaining provider delay elapses')
}
cy.log('Waiting max ' + time_to_wait_ms + 'ms for Krill to respond to the CA create request')
cy.wait('@createCA', { responseTimeout: time_to_wait_ms }).its('response.statusCode').should('eq', expected_status_code)
})
)
})
@@ -1,61 +0,0 @@
describe('OpenID Connect provider connection issues are tolerated', () => {
it('The login form should not be available', () => {
cy.request('POST', 'https://127.0.0.1:1818/test/disable')
cy.wait(500)
cy.visit('/')
cy.url().should('include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form').should('not.exist')
cy.contains('An error occurred while logging you in: OpenID Connect: Cannot get login URL: Failed to connect to provider')
})
it('Login and logout should succeed', () => {
cy.request('POST', 'https://127.0.0.1:1818/test/enable')
cy.wait(500)
// Login
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type('admin')
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('admin')
cy.contains('Sign In').click()
// verify that we are shown to be logged in to the Krill UI
cy.contains('Sign In').should('not.exist')
cy.url().should('include', Cypress.config('baseUrl'))
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains('admin')
// verify that the mock provider thinks the user is logged in
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=admin', failOnStatusCode: false }).its('status').should('eq', 200)
// logout
cy.intercept({ method: 'GET', url: /^https:\/\/localhost:1818\/logout.+/}).as('oidcLogout')
cy.get('.logout').click()
cy.wait('@oidcLogout').its('response.statusCode').should('eq', 302)
// verify that the mock provider thinks the user is now logged out
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=admin', failOnStatusCode: false }).its('status').should('eq', 400)
// verify that we are shown the OpenID Connect provider login page
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]')
})
it('Login should fail to redirect to the discovered but unavailable login page', () => {
cy.request('POST', 'https://127.0.0.1:1818/test/disable')
cy.wait(500)
cy.visit('/')
})
it('The login page should be reachable again', () => {
cy.request('POST', 'https://127.0.0.1:1818/test/enable')
cy.wait(500)
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
})
})
@@ -1,35 +0,0 @@
let username = 'admin@krill';
describe('OpenID Connect provider with custom logout URL', () => {
it('Logout when logged in behaves as expected', () => {
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(username)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('admin')
cy.contains('Sign In').click()
// We should end up back in the Krill UI
cy.url().should('include', Cypress.config('baseUrl'))
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(username)
cy.get('#userinfo_table').contains("role")
// verify that the mock provider thinks the user is logged in
cy.request('https://127.0.0.1:1818/test/is_user_logged_in?username=' + username).its('status').should('eq', 200)
// logout
cy.intercept('https://example.net/', 'custom logout page requested').as('getCustomLogoutURL')
cy.get('.logout').click()
// verify that we are directed to the custom logout URL stub
cy.wait('@getCustomLogoutURL').its('response.statusCode').should('eq', 200)
cy.url().should('eq', 'https://example.net/')
// verify that the mock provider thinks the user is STILL logged in because due to the use of a custom logout URL
// we deliberately did NOT tell the OpenID Connect mock provider that the user should be logged out
cy.request('https://127.0.0.1:1818/test/is_user_logged_in?username=' + username).its('status').should('eq', 200)
})
})
@@ -1,40 +0,0 @@
let username = 'admin@krill';
describe('OpenID Connect provider with fallback logout URL', () => {
it('Logout when logged in behaves as expected', () => {
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(username)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('admin')
cy.contains('Sign In').click()
// We should end up back in the Krill UI
cy.url().should('include', Cypress.config('baseUrl'))
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(username)
cy.get('#userinfo_table').contains("role")
// verify that the mock provider thinks the user is logged in
cy.request('https://127.0.0.1:1818/test/is_user_logged_in?username=' + username).its('status').should('eq', 200)
// logout
cy.intercept({ method: 'POST', path: '/auth/logout'}).as('getLogoutURL')
cy.intercept({ method: 'GET', path: '/index.html'}).as('getLoginForm')
cy.get('.logout').click()
// verify that we are shown the OpenID Connect provider login page
cy.wait('@getLogoutURL').its('response.statusCode').should('eq', 200)
cy.wait('@getLoginForm').its('response.statusCode').should('eq', 200)
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]')
// verify that the mock provider thinks the user is STILL logged in because due to the OpenID Connect mock being
// configured to NOT support end_session_endpoint or revocation_endpoint there is no way to tell the mock that we
// are logging the user out
cy.request('https://127.0.0.1:1818/test/is_user_logged_in?username=' + username).its('status').should('eq', 200)
})
})
@@ -1,61 +0,0 @@
let login_test_settings = [
{ u: 'shorttokenwithrefresh@krill', o: true, refresh: true },
{ u: 'shorttokenwithoutrefresh@krill', o: false, refresh: false }
];
describe('OpenID Connect provider with OAuth 2 revocation', () => {
login_test_settings.forEach(function (ts) {
it('Logout when logged in as user ' + ts.u + ' should ' + (ts.o ? 'successfully' : 'fail to') + ' revoke the token', () => {
cy.intercept({ url: /^https:\/\/localhost:1818\/authorize/ }).as('getLoginForm')
cy.intercept({ url: /^https:\/\/localhost:1818\/login_form_submit/ }).as('submitLoginForm')
cy.intercept({ url: /^https:\/\/localhost:3000\/auth\/callback/ }).as('completeTheLoginInKrill')
cy.intercept({ url: /^https:\/\/localhost:3000\/index\.html/ }).as('afterLoginCompleteInKrill')
cy.visit('/')
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]').clear().type(ts.u)
cy.get('input[name="userattr1"]').clear().type('role') // a role is required to be able to login
cy.get('input[name="userattrval1"]').clear().type('readonly')
if (ts.refresh) {
cy.get('input[name="refresh"]').check() // ensure issuing of refresh tokens for this user
} else {
cy.get('input[name="refresh"]').uncheck() // prevent issuing of refresh tokens for this user
}
cy.contains('Sign In').click()
cy.wait(['@getLoginForm', '@submitLoginForm', '@completeTheLoginInKrill', '@afterLoginCompleteInKrill'])
// We should end up back in the Krill UI
cy.url().should('include', Cypress.config('baseUrl'))
cy.contains('Sign In').should('not.exist')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(ts.u)
cy.get('#userinfo_table').contains("role")
// verify that the mock provider thinks the user is logged in
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=' + ts.u, failOnStatusCode: false }).its('status').should('eq', 200)
// logout, and thus trigger the invocation of the OAuth 2.0 token revocation endpoint
// for users with both a refresh token and an access token first Krill will try to revoke the refresh token
// then will retry if that fails with the access token
cy.intercept({ url: /^https:\/\/localhost:3000\/auth\/logout/ }).as('getLogoutURL')
cy.get('.logout').click()
// verify that we are shown the OpenID Connect provider login page
cy.wait('@getLogoutURL').its('response.statusCode').should('eq', 200)
cy.url().should('not.include', Cypress.config('baseUrl'))
cy.contains('Mock OpenID Connect login form')
cy.get('input[name="username"]')
if (ts.o) {
// verify that the mock provider thinks the user is now logged out
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=' + ts.u, failOnStatusCode: false }).its('status').should('eq', 400)
} else {
// verify that the mock provider thinks the user is still logged in (because it only supports revocation by
// refresh token, not by access token)
cy.request({ url: 'https://127.0.0.1:1818/test/is_user_logged_in?username=' + ts.u, failOnStatusCode: false }).its('status').should('eq', 200)
}
})
})
})
@@ -1,56 +0,0 @@
// Team names and CAs they can access are defined in doc/policies/team-based-access-demo.polar.
// Team memberships and user roles within teams are defined in test-resources/ui/multi_user_team_based_access.conf.
// This test verifies that team roles and team CA rights work as expected, as defined in those files.
// A note about strong password hashing login delays
// -----------------------------------------------------------------------------
// The strong password hashing on the client and server side when logging in with
// config file users causes the login process to take a few seconds. As such we
// extend the default timeout when checking for Sign In completion, like so:
//
// cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
let t1ro = { u: 'team1ro@krill', p: 'team1ro' };
let t1rw = { u: 'team1rw@krill', p: 'team1rw' };
let t2ro = { u: 'team2ro@krill', p: 'team2ro' };
let t2rw = { u: 'team2rw@krill', p: 'team2rw' };
let create_ca_test_settings = [
{ d: 't1ro', u: t1ro.u, p: t1ro.p, o: false, ca: 'ca1', t: 'Red Team', tr: 'Read Only' },
{ d: 't1rw', u: t1rw.u, p: t1rw.p, o: true, ca: 'ca1', t: 'Red Team', tr: 'Read Write' },
{ d: 't2ro', u: t2ro.u, p: t2ro.p, o: false, ca: 'ca2', t: 'Blue Team', tr: 'Read Only' },
{ d: 't2rw', u: t2rw.u, p: t2rw.p, o: true, ca: 'ca2', t: 'Blue Team', tr: 'Read Write' },
];
describe('Config File users with custom team policy', () => {
create_ca_test_settings.forEach(function (ts) {
it('Create CA as ' + ts.d + ' user should ' + (ts.o ? 'succeed' : 'fail'), () => {
cy.visit('/')
cy.get('#login_id').type(ts.u)
cy.get('#login_password').type(ts.p)
cy.contains('Sign In').click()
cy.contains('Sign In', { timeout: 10000 }).should('not.exist')
cy.contains(ts.u)
cy.contains('Welcome to Krill')
// verify our team and role
cy.get('#userinfo')
cy.get('#userinfo').click()
cy.get('#userinfo_table').contains(ts.t)
cy.get('#userinfo_table').contains(ts.tr)
// create a CA
cy.contains('CA Handle')
cy.get('form input[type="text"]').type(ts.ca)
cy.contains('Create CA').click()
cy.contains('OK').click()
// no longer on the welcome page
if (ts.o) {
cy.contains('Welcome to Krill').should('not.exist')
} else {
cy.contains('Welcome to Krill')
}
})
})
})
-99
View File
@@ -1,99 +0,0 @@
// Difficulty entering XML into Lagosta XML input fields:
// ------------------------------------------------------
// Using cy.type() to enter XML into these fields is extremely slow, one
// animated character at a time. I haven't yet found a way to copy-paste into
// them. Setting the text directly can be done but there is a challenge that has
// to be worked around which is that the fields use Prism Editor JS to syntax
// highlight the XML. Prism Editor manages the content as a rich HTML child node
// structure. Just replacing the content with a new text node doesn't work as
// the Lagosta JS code reading the field content doesn't get the content as
// set for some reason. The set text also doesn't get syntax highlighted. What
// seems to work however is causing a keyboard event in the field after the text
// has been set, e.g. pressing the End key.
//
// To summarize, the following works quickly:
// cy.get('... pre[contenteditable="true"]').invoke('text', xml)
// cy.get('... pre[contenteditable="true"]').type('{end}')
//
// This is less hacky but very slow: (even with "type(xml, {delay: 0}))")
// cy.get('... pre[contenteditable="true"]').clear().type(xml)
let publisher_request_test_settings = [
{ desc: 'Compact publisher request XML is accepted', fixture: 'testbed/publisher_request_compact.xml', httpCode: 200 },
{ desc: 'Publisher request with whitespace is accepted', fixture: 'testbed/publisher_request_with_whitespace.xml', httpCode: 200 },
{ desc: 'Publisher request with invalid Base64 certificate is rejected by Lagosta', fixture: 'testbed/publisher_request_invalid_base64.xml', httpCode: 'n/a', errMsg: 'Element <publisher_bpki_ta> must contain a correctly Base64 encoded self-signed X.509 BPKI certificate' },
{ desc: 'Publisher request with unicode space char is rejected by Lagosta', fixture: 'testbed/publisher_request_with_unicode_space_char.xml', httpCode: 'n/a', errMsg: 'Element <publisher_bpki_ta> cannot contain non-ASCII characters' },
{ desc: 'Publisher request with unicode space entity reference is rejected by Lagosta', fixture: 'testbed/publisher_request_with_unicode_space_entity_reference.xml', httpCode: 'n/a', errMsg: 'Element <publisher_bpki_ta> cannot contain non-ASCII characters' },
{ desc: 'Publisher request with unicode space entity reference in handle is rejected by Krill', fixture: 'testbed/publisher_request_with_unicode_space_entity_reference_in_handle.xml', httpCode: 400, errMsg: 'Input contains non-ASCII chars (maybe whitespace?)' },
];
let child_request_test_settings = [
{ desc: 'Compact child request XML is accepted', fixture: 'testbed/child_request_compact.xml', httpCode: 200 },
{ desc: 'Child request with whitespace is accepted', fixture: 'testbed/child_request_with_whitespace.xml', httpCode: 200 },
{ desc: 'Child request with invalid Base64 certificate is rejected by Lagosta', fixture: 'testbed/child_request_invalid_base64.xml', httpCode: 'n/a', errMsg: 'Element <child_bpki_ta> must contain a correctly Base64 encoded self-signed X.509 BPKI certificate' },
{ desc: 'Child request with unicode space char is rejected by Lagosta', fixture: 'testbed/child_request_with_unicode_space_char.xml', httpCode: 'n/a', errMsg: 'Element <child_bpki_ta> cannot contain non-ASCII characters' },
{ desc: 'Child request with unicode space entity reference is rejected by Lagosta', fixture: 'testbed/child_request_with_unicode_space_entity_reference.xml', httpCode: 'n/a', errMsg: 'Element <child_bpki_ta> cannot contain non-ASCII characters' },
{ desc: 'Child request with unicode space entity reference in handle is rejected by Krill', fixture: 'testbed/child_request_with_unicode_space_entity_reference_in_handle.xml', httpCode: 400, errMsg: 'Input contains non-ASCII chars (maybe whitespace?)' },
];
describe('Testbed UI test', () => {
publisher_request_test_settings.forEach(function (ts) {
it(ts.desc, () => {
cy.fixture(ts.fixture).then((xml) => {
// use the local testbed UI to submit the request to register the publisher
cy.visit("/index.html#/testbed")
// verify that the register child tab is active by default
cy.get('#addChild').contains('Child Request XML').should('be.visible')
// enter the request XML into the testbed UI edit field
cy.get('div#tab-addPublisher').contains('Register Publisher').click()
cy.get('#addPublisher pre[contenteditable="true"]').invoke('text', xml)
cy.get('#addPublisher pre[contenteditable="true"]').type('{end}')
cy.intercept({ method: 'POST', path: '/testbed/publishers'}).as('addPublisher')
cy.get('#addPublisher button').contains('Register publisher').click()
if (ts.httpCode != 'n/a') {
cy.get('div[role="dialog"] button').contains('OK').click()
cy.wait('@addPublisher').its('response.statusCode').should('eq', ts.httpCode)
}
if (ts.httpCode == 200) {
cy.contains('has been added to the testbed')
} else {
cy.contains(ts.errMsg)
}
})
})
})
child_request_test_settings.forEach(function (ts) {
it(ts.desc, () => {
cy.fixture(ts.fixture).then((xml) => {
// use the local testbed UI to submit the request to register the child
cy.visit("/index.html#/testbed")
// enter the request XML into the testbed UI edit field
cy.get('div#tab-addChild').contains('Register CA').click()
cy.get('#addChild pre[contenteditable="true"]').invoke('text', xml)
cy.get('#addChild pre[contenteditable="true"]').type('{end}')
cy.get('#addChild input[placeholder^="The AS resources"]').type('AS18')
cy.get('#addChild input[placeholder^="The IPv4 resources"]').type('10.0.0.0/24')
cy.intercept({ method: 'POST', path: '/testbed/children'}).as('addChild')
cy.get('#addChild button').contains('Register child CA').click()
if (ts.httpCode != 'n/a') {
cy.get('div[role="dialog"] button').contains('OK').click()
cy.wait('@addChild').its('response.statusCode').should('eq', ts.httpCode)
}
if (ts.httpCode == 200) {
cy.contains('has been added to the testbed')
} else {
cy.contains(ts.errMsg)
}
})
})
})
})
-58
View File
@@ -1,58 +0,0 @@
// This file is loaded by Cypress because cypress.js (in the root of the Krill
// repository) sets "supportFile" to point to this file.
//
// As advised by Cypress *1, prevent Cypress sometimes failing tests due to
// error "ResizeObserver loop limit exceeded" errors.
//
// *1: jennifer@cypress.io aka https://github.com/jennifer-shehane who wrote
// the following at *2 which was linked from *3:
//
// const resizeObserverLoopErrRe = /^ResizeObserver loop limit exceeded/
//
// Cypress.on('uncaught:exception', (err) => {
// if (resizeObserverLoopErrRe.test(err.message)) {
// // returning false here prevents Cypress from
// // failing the test
// return false
// }
// })
//
// *2: https://github.com/quasarframework/quasar/issues/2233#issuecomment-492975745
// *3: https://github.com/WICG/resize-observer/issues/38#issuecomment-493014026
//
// See also:
// - https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/fundamentals__errors/cypress/integration/app-error.js
// - https://docs.cypress.io/api/events/catalog-of-events.html#Uncaught-Exceptions
// - https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Support-file
// - https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded/63519375#63519375
// - https://github.com/WICG/resize-observer/issues/38
// Define a custom uncaught exception handling policy for Cypress.
// Returning false prevents Cypress from failing the test.
Cypress.on('uncaught:exception', (err, runnable, promise) => {
console.log("Krill UI Test: Examining uncaught exception..")
console.log("Krill UI Test: err: ", err)
if (promise) {
console.log("Krill UI Test: Ignoring unhandled promise rejection.")
return false
}
if (err.message) {
if (err.message.includes('ResizeObserver loop limit exceeded')) {
console.log("Krill UI Test: Ignoring 'ResizeObserver loop limit exceeded' exception")
return false
}
if (err.message.includes('Redirected when going from')) {
// This happens when going from "/onboarding" to "/interstitial" via a navigation guard and is triggered
// when logging out of Krill.
// TODO: Is it safe to ignore this or is this pointing to a real bug in Lagosta?
console.log("Krill UI Test: Ignoring 'Redirected when going from' exception")
return false
}
}
// on any other error message the test fails
console.log("Krill UI Test: Failing the test")
})
-175
View File
@@ -1,175 +0,0 @@
#[cfg(feature = "multi-user")]
mod openid_connect_mock;
use tokio::task;
use OpenIDConnectMockMode::NotStarted;
use std::process::Command;
use std::{env, process::ExitStatus};
use krill::daemon::config::Config;
use krill::test::*;
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum OpenIDConnectMockMode {
NotStarted,
WithRPInitiatedLogout,
WithOAuth2Revocation,
WithNoLogoutEndpoints,
}
pub struct OpenIDConnectMockConfig {
mode: OpenIDConnectMockMode,
enabled_on_startup: bool,
}
#[allow(dead_code)]
impl OpenIDConnectMockConfig {
/// Don't start the OpenID Connect mock.
pub fn do_not_start() -> OpenIDConnectMockConfig {
Self {
mode: NotStarted,
enabled_on_startup: false,
}
}
/// Start the OpenID Mock and enable it ready for use.
pub fn enabled(mode: OpenIDConnectMockMode) -> OpenIDConnectMockConfig {
Self {
mode,
enabled_on_startup: true,
}
}
/// Start the OpenID Mock initially disabled. This can be useful to prevent initial OpenID Connect Discovery
/// succeeding before the first test runs.
pub fn disabled(mode: OpenIDConnectMockMode) -> OpenIDConnectMockConfig {
Self {
mode,
enabled_on_startup: false,
}
}
pub fn mode(&self) -> OpenIDConnectMockMode {
self.mode
}
pub fn enabled_on_startup(&self) -> bool {
self.enabled_on_startup
}
}
#[cfg(not(feature = "multi-user"))]
pub async fn run_krill_ui_test(test_name: &str, _: OpenIDConnectMockConfig) {
assert!(do_run_krill_ui_test(test_name).await);
}
#[cfg(feature = "multi-user")]
pub async fn run_krill_ui_test(test_name: &str, openid_connect_mock_config: OpenIDConnectMockConfig) {
let op_handle = match openid_connect_mock_config.mode() {
NotStarted => None,
_ => Some(openid_connect_mock::start(openid_connect_mock_config, 1).await),
};
let test_result = do_run_krill_ui_test(test_name).await;
if let Some(handle) = op_handle {
openid_connect_mock::stop(handle).await;
}
assert!(test_result);
}
struct CypressRunner {
status: ExitStatus,
}
impl CypressRunner {
pub async fn run(test_name: &str) -> Self {
let test_name = test_name.to_string();
ctrlc::set_handler(move || {
// If `cargo test` is stopped with CTRL-C the background Cypress Docker container continues to run. This
// prevents the next run of `cargo test` from working as the container unexpectedly already exists. Tell
// Docker to kill it to avoid leaving it lying around.
Command::new("docker")
.arg("kill")
.arg("cypress")
.spawn()
.expect("Failed to kill Cypress Docker container");
})
.expect("Error setting Ctrl-C handler");
let task = task::spawn_blocking(move || {
// NOTE: the directory mentioned here must be the same as the directory
// mentioned in the tests/ui/cypress/plugins/index.js file in the
// "integrationFolder" property otherwise Cypress mysteriously complains
// that it cannot find the spec file.
let cypress_spec_path = format!("tests/ui/cypress/specs/{}.js", test_name);
let mut cmd = Command::new("docker");
cmd.arg("run")
.arg("--name")
.arg("cypress")
.arg("--rm")
.arg("--net=host")
.arg("--ipc=host")
.arg("-v")
.arg(format!("{}:/e2e", env::current_dir().unwrap().display()))
.arg("-w")
.arg("/e2e");
if let Ok(debug_level) = std::env::var("CYPRESS_DEBUG") {
// Example values:
// - To get LOTS of Cypress logging: CYPRESS_DEBUG=cypress:*
// - To get logging relating to HTTP requests: CYPRESS_DEBUG=cypress:proxy:http:*
cmd.arg("-e").arg(format!("DEBUG={}", debug_level));
}
if std::env::var("CYPRESS_INTERACTIVE").is_ok() {
// After running `cargo test` a Chrome browser should open from the Cypress Docker container on your local
// X server. For this to work you might need to run this command in your shell prior to `cargo test`:
// xhost +
cmd.arg("-v")
.arg("/tmp/.X11-unix:/tmp/.X11-unix")
.arg("-e")
.arg("DISPLAY")
.arg("--entrypoint")
.arg("cypress");
}
cmd.arg("cypress/included:8.1.0");
if std::env::var("CYPRESS_INTERACTIVE").is_ok() {
cmd.arg("open").arg("--project").arg(".");
} else {
cmd.arg("--spec").arg(cypress_spec_path);
}
cmd.arg("--browser")
.arg("chrome")
.status()
.expect("Failed to run Cypress Docker UI test suite")
})
.await;
Self { status: task.unwrap() }
}
pub fn success(self) -> bool {
self.status.success()
}
}
async fn do_run_krill_ui_test(test_name: &str) -> bool {
krill::constants::enable_test_mode();
let config_path = &format!("test-resources/ui/{}.conf", test_name);
let config = Config::read_config(config_path).unwrap();
// Start Krill as a Tokio task in the background and wait just until we can tell that it has started.
start_krill_with_custom_config(config).await;
// Run the specified Cypress UI test suite and wait for it to finish
CypressRunner::run(test_name).await.success()
}
File diff suppressed because it is too large Load Diff