test(tmachine): migrate Keycloak provider refresh coverage (#3404)

* test(tmachine): add Keycloak provider refresh suite

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(tmachine): share container runtime detection

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* ci(tmachine): run feature suites in GitHub Actions

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* ci(tmachine): run conformance with Podman tests

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* ci(tmachine): cover provider refresh with Podman

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* ci(integration): split input preparation from runners

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
Evan Lezar
2026-09-18 13:56:07 +02:00
committed by GitHub
parent 473d1e9974
commit 2263685cf3
25 changed files with 2746 additions and 265 deletions
@@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
---
- name: Run Keycloak provider refresh tests
hosts: all
gather_facts: false
roles:
- keycloak
tasks:
- name: Wait for SSH
ansible.builtin.wait_for_connection:
- name: Create Keycloak provider refresh test directory
become: true
ansible.builtin.file:
path: /var/lib/openshell-provider-refresh/tests
state: directory
owner: tmachine
group: tmachine
mode: "0700"
- name: Install Keycloak provider refresh test bundle
become: true
ansible.builtin.copy:
src: "{{ provider_refresh_keycloak_test_bundle }}"
dest: /var/lib/openshell-provider-refresh/tests/bundle.tar
owner: tmachine
group: tmachine
mode: "0600"
- name: Extract Keycloak provider refresh test bundle
become: true
ansible.builtin.unarchive:
src: /var/lib/openshell-provider-refresh/tests/bundle.tar
dest: /var/lib/openshell-provider-refresh/tests
owner: tmachine
group: tmachine
remote_src: true
- name: Check Keycloak provider refresh test archive
ansible.builtin.stat:
path: /var/lib/openshell-provider-refresh/tests/tests.tar.zst
register: provider_refresh_keycloak_test_archive
- name: Require Keycloak provider refresh test archive
ansible.builtin.assert:
that:
- provider_refresh_keycloak_test_archive.stat.isreg | default(false)
fail_msg: Keycloak provider refresh test bundle did not contain tests.tar.zst
- name: Run Keycloak provider refresh archive
ansible.builtin.command:
argv:
- cargo-nextest
- nextest
- run
- --archive-file
- /var/lib/openshell-provider-refresh/tests/tests.tar.zst
- --workspace-remap
- /var/lib/openshell-provider-refresh/tests
- --no-capture
environment:
OPENSHELL_BIN: /usr/local/bin/openshell
OPENSHELL_E2E_OIDC_ISSUER: http://127.0.0.1:8180/realms/openshell
OPENSHELL_E2E_OIDC_USERNAME: admin@test
OPENSHELL_E2E_OIDC_PASSWORD: admin
register: provider_refresh_result
changed_when: false
failed_when: false
- name: Show Keycloak provider refresh result
ansible.builtin.debug:
var: provider_refresh_result
when: provider_refresh_result.rc != 0
- name: Read OpenShell gateway logs
become: true
ansible.builtin.command:
argv:
- journalctl
- --unit
- openshell-gateway.service
- --no-pager
- --lines
- "500"
register: openshell_gateway_logs
changed_when: false
failed_when: false
when: provider_refresh_result.rc != 0
- name: Show OpenShell gateway logs
ansible.builtin.debug:
var: openshell_gateway_logs.stdout_lines
when: provider_refresh_result.rc != 0
- name: Require Keycloak provider refresh success
ansible.builtin.assert:
that: provider_refresh_result.rc == 0
fail_msg: Keycloak provider refresh test failed
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
---
# Keep these values aligned with scripts/keycloak-dev.sh. The role provisions
# an isolated guest fixture, while the script manages a developer-host fixture.
keycloak_container_name: openshell-keycloak
keycloak_image: quay.io/keycloak/keycloak:24.0
keycloak_port: 8180
keycloak_realm: openshell
keycloak_admin_username: admin
keycloak_admin_password: admin
keycloak_state_directory: /var/lib/openshell-keycloak
keycloak_health_retries: 45
keycloak_health_delay: 2
@@ -0,0 +1,126 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
---
- name: Wait for SSH
ansible.builtin.wait_for_connection:
- name: Detect tmachine container runtime
ansible.builtin.include_role:
name: tmachine_container_runtime
- name: Set Keycloak runtime values
ansible.builtin.set_fact:
keycloak_runtime_user: >-
{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}
keycloak_runtime_home: >-
{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}
keycloak_runtime_state_directory: >-
{{ '/home/tmachine/.local/share/openshell-keycloak'
if tmachine_container_runtime_is_rootless else keycloak_state_directory }}
- name: Require Keycloak container runtime
become: true
become_user: "{{ keycloak_runtime_user }}"
ansible.builtin.command:
argv:
- "{{ tmachine_container_runtime_name }}"
- --version
environment:
HOME: "{{ keycloak_runtime_home }}"
changed_when: false
- name: Create Keycloak state directory
become: true
ansible.builtin.file:
path: "{{ keycloak_runtime_state_directory }}"
state: directory
owner: "{{ keycloak_runtime_user }}"
group: "{{ keycloak_runtime_user }}"
mode: "0755"
- name: Install Keycloak realm fixture
become: true
ansible.builtin.copy:
src: "{{ keycloak_realm_file }}"
dest: "{{ keycloak_runtime_state_directory }}/realm.json"
owner: "{{ keycloak_runtime_user }}"
group: "{{ keycloak_runtime_user }}"
mode: "0644"
- name: Remove an existing Keycloak fixture
become: true
become_user: "{{ keycloak_runtime_user }}"
ansible.builtin.command:
argv:
- "{{ tmachine_container_runtime_name }}"
- rm
- --force
- "{{ keycloak_container_name }}"
environment:
HOME: "{{ keycloak_runtime_home }}"
changed_when: false
failed_when: false
- name: Start Keycloak fixture
become: true
become_user: "{{ keycloak_runtime_user }}"
ansible.builtin.command:
argv:
- "{{ tmachine_container_runtime_name }}"
- run
- --detach
- --name
- "{{ keycloak_container_name }}"
- --publish
- "127.0.0.1:{{ keycloak_port }}:8080"
- --env
- "KEYCLOAK_ADMIN={{ keycloak_admin_username }}"
- --env
- "KEYCLOAK_ADMIN_PASSWORD={{ keycloak_admin_password }}"
- --volume
- "{{ keycloak_runtime_state_directory }}/realm.json:/opt/keycloak/data/import/realm.json:ro,z"
- "{{ keycloak_image }}"
- start-dev
- --import-realm
environment:
HOME: "{{ keycloak_runtime_home }}"
changed_when: true
- name: Wait for Keycloak discovery endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ keycloak_port }}/realms/{{ keycloak_realm }}/.well-known/openid-configuration"
status_code: 200
return_content: false
register: keycloak_discovery
retries: "{{ keycloak_health_retries }}"
delay: "{{ keycloak_health_delay }}"
until: keycloak_discovery.status | default(0) == 200
failed_when: false
- name: Read Keycloak logs after failed health check
become: true
become_user: "{{ keycloak_runtime_user }}"
ansible.builtin.command:
argv:
- "{{ tmachine_container_runtime_name }}"
- logs
- --tail
- "30"
- "{{ keycloak_container_name }}"
environment:
HOME: "{{ keycloak_runtime_home }}"
register: keycloak_logs
changed_when: false
failed_when: false
when: keycloak_discovery.status | default(0) != 200
- name: Show Keycloak logs after failed health check
ansible.builtin.debug:
var: keycloak_logs.stdout_lines
when: keycloak_discovery.status | default(0) != 200
- name: Require Keycloak discovery endpoint
ansible.builtin.assert:
that: keycloak_discovery.status | default(0) == 200
fail_msg: Keycloak did not become healthy within the configured timeout
@@ -2,50 +2,21 @@
# SPDX-License-Identifier: Apache-2.0
---
- name: Resolve tmachine UID
ansible.builtin.command:
argv:
- id
- -u
- tmachine
changed_when: false
register: openshell_tmachine_uid
- name: Detect tmachine container runtime
ansible.builtin.include_role:
name: tmachine_container_runtime
- name: Check Docker socket
become: true
ansible.builtin.stat:
path: /var/run/docker.sock
register: openshell_docker_socket
- name: Check rootful Podman socket
become: true
ansible.builtin.stat:
path: /run/podman/podman.sock
register: openshell_rootful_podman_socket
- name: Check rootless Podman socket
become: true
ansible.builtin.stat:
path: "/run/user/{{ openshell_tmachine_uid.stdout }}/podman/podman.sock"
register: openshell_rootless_podman_socket
- name: Require exactly one container runtime socket
ansible.builtin.assert:
that:
- >-
(openshell_docker_socket.stat.exists | int)
+ (openshell_rootful_podman_socket.stat.exists | int)
+ (openshell_rootless_podman_socket.stat.exists | int) == 1
fail_msg: Expected exactly one Docker or Podman socket
- name: Select container runtime
- name: Set OpenShell gateway runtime values
ansible.builtin.set_fact:
openshell_gateway_driver: "{{ 'docker' if openshell_docker_socket.stat.exists else 'podman' }}"
openshell_gateway_user: "{{ 'root' if openshell_rootful_podman_socket.stat.exists else 'tmachine' }}"
openshell_gateway_home: "{{ '/root' if openshell_rootful_podman_socket.stat.exists else '/home/tmachine' }}"
openshell_gateway_uid: "{{ '0' if openshell_rootful_podman_socket.stat.exists else openshell_tmachine_uid.stdout }}"
openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if openshell_docker_socket.stat.exists else '0.0.0.0:17670' }}"
openshell_runtime_socket: >-
{{ '/var/run/docker.sock' if openshell_docker_socket.stat.exists
else '/run/podman/podman.sock' if openshell_rootful_podman_socket.stat.exists
else '/run/user/' ~ openshell_tmachine_uid.stdout ~ '/podman/podman.sock' }}
openshell_gateway_driver: "{{ tmachine_container_runtime_name }}"
openshell_gateway_user: >-
{{ 'root' if tmachine_container_runtime_name == 'podman'
and not tmachine_container_runtime_is_rootless else 'tmachine' }}
openshell_gateway_home: >-
{{ '/root' if tmachine_container_runtime_name == 'podman'
and not tmachine_container_runtime_is_rootless else '/home/tmachine' }}
openshell_gateway_uid: >-
{{ '0' if tmachine_container_runtime_name == 'podman'
and not tmachine_container_runtime_is_rootless else tmachine_container_runtime_tmachine_uid.stdout }}
openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if tmachine_container_runtime_name == 'docker' else '0.0.0.0:17670' }}"
openshell_runtime_socket: "{{ tmachine_container_runtime_socket }}"
@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
---
- name: Resolve tmachine UID
ansible.builtin.command:
argv:
- id
- -u
- tmachine
changed_when: false
register: tmachine_container_runtime_tmachine_uid
- name: Check Docker socket
become: true
ansible.builtin.stat:
path: /var/run/docker.sock
register: tmachine_container_runtime_docker_socket
- name: Check rootful Podman socket
become: true
ansible.builtin.stat:
path: /run/podman/podman.sock
register: tmachine_container_runtime_rootful_podman_socket
- name: Check rootless Podman socket
become: true
ansible.builtin.stat:
path: "/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}/podman/podman.sock"
register: tmachine_container_runtime_rootless_podman_socket
- name: Require exactly one container runtime socket
ansible.builtin.assert:
that:
- >-
(tmachine_container_runtime_docker_socket.stat.exists | int)
+ (tmachine_container_runtime_rootful_podman_socket.stat.exists | int)
+ (tmachine_container_runtime_rootless_podman_socket.stat.exists | int) == 1
fail_msg: Expected exactly one Docker or Podman socket
- name: Select tmachine container runtime
ansible.builtin.set_fact:
tmachine_container_runtime_name: >-
{{ 'docker' if tmachine_container_runtime_docker_socket.stat.exists else 'podman' }}
tmachine_container_runtime_is_rootless: >-
{{ tmachine_container_runtime_rootless_podman_socket.stat.exists }}
tmachine_container_runtime_socket: >-
{{ '/var/run/docker.sock' if tmachine_container_runtime_docker_socket.stat.exists
else '/run/podman/podman.sock' if tmachine_container_runtime_rootful_podman_socket.stat.exists
else '/run/user/' ~ tmachine_container_runtime_tmachine_uid.stdout ~ '/podman/podman.sock' }}
+23 -3
View File
@@ -84,9 +84,17 @@ let
target = muslToolchain.target;
output = "artifacts/test-archives/${muslToolchain.target}/openshell-conformance-tests.tar";
};
providerRefreshKeycloakArchive = mkTestArchive {
name = "provider-refresh-keycloak";
workspacePath = "tests/suites/features";
manifestPath = "tests/suites/features/Cargo.toml";
package = "openshell-test-feature-provider-refresh-keycloak";
target = muslToolchain.target;
output = "artifacts/test-archives/${muslToolchain.target}/provider-refresh-keycloak-tests.tar";
};
in
rec {
inherit conformanceCliArchive;
inherit conformanceCliArchive providerRefreshKeycloakArchive;
binaries = pkgs.writeShellApplication {
name = "build-artifacts-binaries";
@@ -126,6 +134,18 @@ rec {
'';
};
testArchives = pkgs.writeShellApplication {
name = "build-artifacts-test-archives";
runtimeInputs = [
conformanceCliArchive
providerRefreshKeycloakArchive
];
text = ''
build-openshell-conformance-test-archive
build-provider-refresh-keycloak-test-archive
'';
};
images = pkgs.writeShellApplication {
name = "build-artifacts-images";
runtimeInputs = [
@@ -204,13 +224,13 @@ rec {
name = "build-artifacts";
runtimeInputs = [
binaries
conformanceCliArchive
testArchives
images
helm
];
text = ''
build-artifacts-binaries
build-openshell-conformance-test-archive
build-artifacts-test-archives
build-artifacts-images
build-artifacts-helm
'';
+8
View File
@@ -114,6 +114,14 @@ let
openshell_conformance_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-conformance-tests.tar";
};
}
{
name = "provider-refresh";
playbooks = [ "ansible/playbooks/features/provider-refresh/keycloak.yaml" ];
inputs = {
keycloak_realm_file = "../scripts/keycloak-realm.json";
provider_refresh_keycloak_test_bundle = "../artifacts/test-archives/${muslTarget}/provider-refresh-keycloak-tests.tar";
};
}
];
};
+1868
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[workspace]
resolver = "2"
members = ["provider-refresh/keycloak"]
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[package]
name = "openshell-test-feature-provider-refresh-keycloak"
version = "0.0.0"
edition = "2024"
[dependencies]
openshell-conformance = { path = "../../../../../crates/openshell-conformance" }
openshell-e2e = { path = "../../../../../e2e/rust" }
serde_json = "1"
tempfile = "3"
tokio = { version = "1.43", features = ["macros", "process", "io-util", "rt"] }
@@ -0,0 +1,340 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
#![cfg(target_os = "linux")]
//! Provider OAuth refresh recovery against Keycloak.
//!
//! OpenShell itself uses the local gateway's mTLS authentication. Keycloak is
//! only the provider token issuer: the test refreshes a valid grant, revokes
//! its Keycloak session, and verifies that the gateway reports the next
//! refresh as requiring user reauthorization.
use std::io::Write as _;
use std::process::{Output, Stdio};
use openshell_e2e::harness::binary::openshell_cmd;
use serde_json::Value;
use tempfile::{Builder as TempFileBuilder, NamedTempFile};
use tokio::io::AsyncWriteExt as _;
use tokio::process::Command;
const PROVIDER_NAME: &str = "e2e-keycloak-refresh";
const PROFILE_ID: &str = "e2e-keycloak-refresh";
const CREDENTIAL_KEY: &str = "KEYCLOAK_ACCESS_TOKEN";
fn combined_output(output: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
async fn run_cli(args: &[&str], env: &[(&str, &str)]) -> Result<Output, String> {
openshell_cmd()
.args(args)
.env("NO_COLOR", "1")
.envs(env.iter().copied())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.map_err(|error| format!("run openshell command: {error}"))
}
async fn run_cli_success(args: &[&str], env: &[(&str, &str)]) -> Result<String, String> {
let output = run_cli(args, env).await?;
let combined = combined_output(&output);
if !output.status.success() {
return Err(format!(
"openshell command failed (exit {:?}):\n{combined}",
output.status.code()
));
}
Ok(combined)
}
async fn acquire_keycloak_grant(
issuer: &str,
username: &str,
password: &str,
) -> Result<(String, String), String> {
let token_endpoint = format!("{issuer}/protocol/openid-connect/token");
let username_form = format!("username={username}");
let password_form = format!("password={password}");
let output = Command::new("curl")
.args([
"--fail",
"--silent",
"--show-error",
"--request",
"POST",
&token_endpoint,
"--data-urlencode",
"grant_type=password",
"--data-urlencode",
"client_id=openshell-cli",
"--data-urlencode",
&username_form,
"--data-urlencode",
&password_form,
"--data-urlencode",
"scope=openid",
])
.output()
.await
.map_err(|error| format!("request Keycloak grant: {error}"))?;
if !output.status.success() {
return Err(format!(
"Keycloak grant request failed (exit {:?}): {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
));
}
let response: Value = serde_json::from_slice(&output.stdout)
.map_err(|error| format!("decode Keycloak grant response: {error}"))?;
let access_token = response
.get("access_token")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| "Keycloak grant response omitted access_token".to_string())?;
let refresh_token = response
.get("refresh_token")
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| "Keycloak grant response omitted refresh_token".to_string())?;
Ok((access_token.to_string(), refresh_token.to_string()))
}
async fn revoke_keycloak_grant(issuer: &str, refresh_token: &str) -> Result<(), String> {
let logout_endpoint = format!("{issuer}/protocol/openid-connect/logout");
let mut child = Command::new("curl")
.args([
"--fail",
"--silent",
"--show-error",
"--output",
"/dev/null",
"--request",
"POST",
&logout_endpoint,
"--data-urlencode",
"client_id=openshell-cli",
"--data-urlencode",
"refresh_token@-",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|error| format!("start Keycloak logout request: {error}"))?;
child
.stdin
.take()
.ok_or_else(|| "Keycloak logout stdin was not piped".to_string())?
.write_all(refresh_token.as_bytes())
.await
.map_err(|error| format!("write Keycloak logout request: {error}"))?;
let output = child
.wait_with_output()
.await
.map_err(|error| format!("wait for Keycloak logout request: {error}"))?;
if !output.status.success() {
return Err(format!(
"Keycloak logout failed (exit {:?}): {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
fn write_profile(issuer: &str) -> Result<NamedTempFile, String> {
let mut file = TempFileBuilder::new()
.suffix(".yaml")
.tempfile()
.map_err(|error| format!("create provider profile: {error}"))?;
let profile = format!(
r#"id: {PROFILE_ID}
display_name: Keycloak provider refresh E2E
category: other
credentials:
- name: access_token
env_vars: [{CREDENTIAL_KEY}]
required: true
auth_style: bearer
header_name: authorization
refresh:
strategy: oauth2_refresh_token
token_url: {issuer}/protocol/openid-connect/token
scopes: [openid]
refresh_before_seconds: 60
max_lifetime_seconds: 3600
material:
- name: client_id
required: true
- name: refresh_token
required: true
secret: true
endpoints:
- host: keycloak.test.invalid
port: 443
protocol: rest
access: read-only
enforcement: enforce
binaries:
- /usr/bin/curl
"#
);
file.write_all(profile.as_bytes())
.map_err(|error| format!("write provider profile: {error}"))?;
file.flush()
.map_err(|error| format!("flush provider profile: {error}"))?;
Ok(file)
}
async fn delete_provider_resources() {
let _ = run_cli(&["provider", "delete", PROVIDER_NAME], &[]).await;
let _ = run_cli(&["provider", "profile", "delete", PROFILE_ID], &[]).await;
}
#[tokio::test]
async fn revoked_refresh_grant_requires_user_reauthorization() -> Result<(), String> {
let issuer = std::env::var("OPENSHELL_E2E_OIDC_ISSUER")
.map_err(|_| "OPENSHELL_E2E_OIDC_ISSUER is required".to_string())?;
let username = std::env::var("OPENSHELL_E2E_OIDC_USERNAME")
.map_err(|_| "OPENSHELL_E2E_OIDC_USERNAME is required".to_string())?;
let password = std::env::var("OPENSHELL_E2E_OIDC_PASSWORD")
.map_err(|_| "OPENSHELL_E2E_OIDC_PASSWORD is required".to_string())?;
let (access_token, refresh_token) =
acquire_keycloak_grant(&issuer, &username, &password).await?;
let profile = write_profile(&issuer)?;
let profile_path = profile.path().to_string_lossy().into_owned();
delete_provider_resources().await;
let result = async {
run_cli_success(
&["provider", "profile", "import", "--file", &profile_path],
&[],
)
.await?;
run_cli_success(
&[
"provider",
"create",
"--name",
PROVIDER_NAME,
"--type",
PROFILE_ID,
"--credential",
CREDENTIAL_KEY,
],
&[(CREDENTIAL_KEY, &access_token)],
)
.await?;
run_cli_success(
&[
"provider",
"refresh",
"configure",
PROVIDER_NAME,
"--credential-key",
CREDENTIAL_KEY,
"--strategy",
"oauth2-refresh-token",
"--material",
"client_id=openshell-cli",
"--secret-material-env",
"refresh_token=KEYCLOAK_REFRESH_TOKEN",
],
&[("KEYCLOAK_REFRESH_TOKEN", &refresh_token)],
)
.await?;
run_cli_success(
&[
"provider",
"refresh",
"rotate",
PROVIDER_NAME,
"--credential-key",
CREDENTIAL_KEY,
],
&[],
)
.await?;
let valid_status = run_cli_success(
&[
"provider",
"refresh",
"status",
PROVIDER_NAME,
"--credential-key",
CREDENTIAL_KEY,
],
&[],
)
.await?;
if !valid_status.contains("refreshed") {
return Err(format!(
"valid Keycloak refresh did not reach refreshed state:\n{valid_status}"
));
}
revoke_keycloak_grant(&issuer, &refresh_token).await?;
let failed_rotation = run_cli(
&[
"provider",
"refresh",
"rotate",
PROVIDER_NAME,
"--credential-key",
CREDENTIAL_KEY,
],
&[],
)
.await?;
let failed_rotation_output = combined_output(&failed_rotation);
if failed_rotation.status.success() || !failed_rotation_output.contains("invalid_grant") {
return Err(format!(
"revoked Keycloak refresh did not fail with invalid_grant:\n{failed_rotation_output}"
));
}
let revoked_status = run_cli_success(
&[
"provider",
"refresh",
"status",
PROVIDER_NAME,
"--credential-key",
CREDENTIAL_KEY,
],
&[],
)
.await?;
for expected in [
"reauthorization_required",
"reauthorize",
"oauth_invalid_grant",
] {
if !revoked_status.contains(expected) {
return Err(format!(
"revoked refresh status omitted {expected}:\n{revoked_status}"
));
}
}
if revoked_status.contains("292278994") {
return Err(format!(
"parked refresh rendered the i64::MAX scheduling sentinel as a date:\n{revoked_status}"
));
}
Ok(())
}
.await;
delete_provider_resources().await;
result
}