use autumn_admin_plugin::tokens::TokenAdminModel;
use autumn_admin_plugin::{AdminModel, ListParams, SortDirection};
#[path = "support/pg_fixture.rs"]
mod pg_fixture;
const CREATE_TABLE_SQL: &str = "
CREATE TABLE IF NOT EXISTS api_tokens (
id BIGSERIAL PRIMARY KEY,
token_hash TEXT NOT NULL UNIQUE,
principal_id TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMP,
name TEXT NOT NULL DEFAULT '',
scopes JSONB NOT NULL DEFAULT '[]'::jsonb,
expires_at TIMESTAMP,
last_used_at TIMESTAMP
)
";
async fn setup() -> pg_fixture::PgFixture {
pg_fixture::setup(CREATE_TABLE_SQL).await
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_create_returns_raw_token_and_get_round_trips() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let data = serde_json::json!({
"principal_id": "service:ci",
"name": "ci-token",
"scopes": "[\"posts:read\",\"posts:write\"]",
"expires_at": ""
});
let created = model.create(pool, data).await.unwrap();
let raw_token = created
.get("token")
.and_then(|v| v.as_str())
.expect("token field");
assert!(!raw_token.is_empty(), "raw token must not be empty");
assert!(created.get("token_hash").is_none());
let id = created["id"].as_i64().expect("id");
let fetched = model.get(pool, id).await.unwrap().expect("record");
assert_eq!(fetched["name"], "ci-token");
assert_eq!(fetched["principal_id"], "service:ci");
assert!(
fetched.get("token").is_none(),
"raw token must not reappear"
);
let scopes = fetched["scopes"].as_array().expect("scopes array");
assert!(scopes.iter().any(|s| s == "posts:read"));
assert!(scopes.iter().any(|s| s == "posts:write"));
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_get_returns_none_for_unknown_id() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let result = model.get(pool, 9_999_999).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_list_paginates_and_searches() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
for i in 0..3u32 {
model
.create(
pool,
serde_json::json!({
"principal_id": format!("service:{i}"),
"name": format!("token-{i}"),
"scopes": "[]",
}),
)
.await
.unwrap();
}
let result = model
.list(
pool,
ListParams {
page: 1,
per_page: 10,
search: None,
sort_by: None,
sort_dir: SortDirection::default(),
filters: Vec::new(),
},
)
.await
.unwrap();
assert_eq!(result.total, 3);
assert_eq!(result.records.len(), 3);
let result = model
.list(
pool,
ListParams {
page: 1,
per_page: 10,
search: Some("token-1".into()),
sort_by: None,
sort_dir: SortDirection::default(),
filters: Vec::new(),
},
)
.await
.unwrap();
assert_eq!(result.total, 1);
assert_eq!(result.records[0]["name"], "token-1");
let result = model
.list(
pool,
ListParams {
page: 1,
per_page: 10,
search: Some("service:2".into()),
sort_by: None,
sort_dir: SortDirection::default(),
filters: Vec::new(),
},
)
.await
.unwrap();
assert_eq!(result.total, 1);
let result = model
.list(
pool,
ListParams {
page: 1,
per_page: 2,
search: None,
sort_by: None,
sort_dir: SortDirection::default(),
filters: Vec::new(),
},
)
.await
.unwrap();
assert_eq!(result.total, 3);
assert_eq!(result.records.len(), 2);
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_list_per_page_zero_returns_every_record() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
for i in 0..30u32 {
model
.create(
pool,
serde_json::json!({
"principal_id": format!("service:{i}"),
"name": format!("token-{i}"),
"scopes": "[]",
}),
)
.await
.unwrap();
}
let result = model
.list(
pool,
ListParams {
page: 1,
per_page: 0,
search: None,
sort_by: None,
sort_dir: SortDirection::default(),
filters: Vec::new(),
},
)
.await
.unwrap();
assert_eq!(result.total, 30);
assert_eq!(result.records.len(), 30);
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_update_changes_name_and_scopes() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let created = model
.create(
pool,
serde_json::json!({
"principal_id": "service:ci",
"name": "original",
"scopes": "[\"posts:read\"]",
}),
)
.await
.unwrap();
let id = created["id"].as_i64().expect("id");
let updated = model
.update(
pool,
id,
serde_json::json!({
"name": "updated",
"scopes": "[\"posts:read\",\"posts:write\"]",
}),
)
.await
.unwrap();
assert_eq!(updated["name"], "updated");
let scopes = updated["scopes"].as_array().expect("scopes");
assert_eq!(scopes.len(), 2);
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_delete_revokes_token() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let created = model
.create(
pool,
serde_json::json!({
"principal_id": "service:ci",
"name": "to-revoke",
"scopes": "[]",
}),
)
.await
.unwrap();
let id = created["id"].as_i64().expect("id");
let before = model.get(pool, id).await.unwrap().unwrap();
assert!(before["revoked_at"].is_null());
model.delete(pool, id).await.unwrap();
let after = model.get(pool, id).await.unwrap().unwrap();
assert!(!after["revoked_at"].is_null(), "revoked_at must be set");
model.delete(pool, id).await.unwrap();
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_create_requires_principal_id() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let err = model
.create(pool, serde_json::json!({"name": "x", "scopes": "[]"}))
.await
.unwrap_err();
assert!(
matches!(err, autumn_admin_plugin::AdminError::Validation(_)),
"expected Validation error, got: {err:?}"
);
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_create_accepts_rfc3339_expires_at() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let created = model
.create(
pool,
serde_json::json!({
"principal_id": "service:ci",
"name": "expiring",
"scopes": "[]",
"expires_at": "2030-01-01T00:00:00Z",
}),
)
.await
.unwrap();
let id = created["id"].as_i64().expect("id");
let fetched = model.get(pool, id).await.unwrap().unwrap();
assert!(!fetched["expires_at"].is_null(), "expires_at must be set");
}
#[tokio::test]
#[ignore = "requires Docker (testcontainers), or a URL in AUTUMN_ADMIN_TEST_PG_URL"]
async fn token_admin_bulk_delete_revokes_every_submitted_id() {
let fixture = setup().await;
let pool = &fixture.pool;
let model = TokenAdminModel;
let mut ids = Vec::new();
for i in 0..3u32 {
let created = model
.create(
pool,
serde_json::json!({
"principal_id": format!("service:{i}"),
"name": format!("bulk-{i}"),
"scopes": "[]",
}),
)
.await
.expect("create");
ids.push(created["id"].as_i64().expect("id"));
}
ids.push(9_999_999);
let applied = model
.execute_action(pool, "delete", ids.clone())
.await
.expect("bulk delete");
assert_eq!(
applied,
ids.len() as u64,
"the count reports ids submitted, not rows revoked"
);
for id in ids.iter().take(3) {
let fetched = model.get(pool, *id).await.expect("get").expect("record");
assert!(
!fetched["revoked_at"].is_null(),
"every submitted token must be revoked"
);
}
}