Lines
100 %
Functions
Branches
use super::*;
use crate::db::DB_POOL;
use crate::user::User;
use sqlx::PgPool;
use supp_macro::local_db_sqlx_test;
use tokio::sync::OnceCell;
static CONTEXT: OnceCell<()> = OnceCell::const_new();
static USER: OnceCell<User> = OnceCell::const_new();
async fn setup() {
CONTEXT
.get_or_init(|| async {
#[cfg(feature = "testlog")]
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
})
.await;
USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
}
const TEST_KEY_TYPE: &str = "ssh-ed25519";
const TEST_FP_ALICE: &str = "SHA256:alicefingerprint000000000000000000000000000";
const TEST_FP_BOB: &str = "SHA256:bobfingerprint00000000000000000000000000000";
#[local_db_sqlx_test]
async fn adds_and_lists_a_key(pool: PgPool) -> anyhow::Result<()> {
let user = USER.get().unwrap();
user.commit().await.unwrap();
AddSshKey::new()
.user_id(user.id)
.key_type(TEST_KEY_TYPE.to_string())
.key_blob(vec![1, 2, 3, 4])
.fingerprint(TEST_FP_ALICE.to_string())
.annotation("alice laptop".to_string())
.run()
.await?;
let Some(CmdResult::SshKeys(keys)) = ListSshKeys::new().user_id(user.id).run().await? else {
panic!("expected SshKeys");
};
assert_eq!(keys.len(), 1);
assert_eq!(keys[0].fingerprint, TEST_FP_ALICE);
assert_eq!(keys[0].annotation, "alice laptop");
assert_eq!(keys[0].key_blob, vec![1, 2, 3, 4]);
async fn add_is_idempotent_on_fingerprint(pool: PgPool) -> anyhow::Result<()> {
.key_blob(vec![1, 2, 3])
.annotation("first".to_string())
.annotation("second".to_string())
assert_eq!(keys.len(), 1, "second add should update in place");
assert_eq!(keys[0].annotation, "second");
async fn add_flips_ssh_enabled(pool: PgPool) -> anyhow::Result<()> {
.key_blob(vec![7])
let Some(CmdResult::Uuid(uid)) = LookupUserBySshKey::new()
.await?
else {
panic!("expected Uuid");
assert_eq!(uid, user.id);
async fn remove_flips_ssh_enabled_when_last_key_goes(pool: PgPool) -> anyhow::Result<()> {
.key_blob(vec![1])
RemoveSshKey::new()
let lookup = LookupUserBySshKey::new()
assert!(lookup.is_none(), "deleted key should not resolve");
async fn remove_keeps_ssh_enabled_when_other_keys_remain(pool: PgPool) -> anyhow::Result<()> {
.key_blob(vec![2])
.fingerprint(TEST_FP_BOB.to_string())
// Bob still resolves — ssh_enabled must still be true.
assert!(matches!(lookup, Some(CmdResult::Uuid(_))));
/// `ssh_enabled` stays TRUE — it tracks key presence, not usability.
async fn lookup_refuses_a_disabled_user(pool: PgPool) -> anyhow::Result<()> {
setup().await;
let user = User { id: Uuid::new_v4() };
user.commit().await?;
.key_blob(vec![9, 9])
assert!(matches!(
LookupUserBySshKey::new()
.await?,
Some(CmdResult::Uuid(_))
));
disable(&pool, user.id).await?;
assert!(
.is_none()
);
/// `AddSshKey` is covered by the lock-ordering test below.
async fn remove_refuses_a_disabled_user(pool: PgPool) -> anyhow::Result<()> {
let removed = RemoveSshKey::new()
assert!(matches!(removed, Err(CmdError::Disabled)));
/// The window a plain `SELECT` then `INSERT` would leave open: the check
/// reads TRUE, the disable commits, and the key lands anyway.
async fn add_refuses_a_disable_that_lands_mid_write(pool: PgPool) -> anyhow::Result<()> {
// Take the row first so the command blocks rather than races: the
// assertion is about ordering, not timing.
let mut blocker = pool.begin().await?;
sqlx::query("SELECT verified FROM users WHERE id = $1 FOR UPDATE")
.bind(user.id)
.fetch_one(&mut *blocker)
let user_id = user.id;
let adding = tokio::spawn(async move {
.user_id(user_id)
.key_blob(vec![4, 2])
.await
});
// Let the command reach the lock, then switch the account off under it.
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
sqlx::query("UPDATE users SET verified = FALSE WHERE id = $1")
.execute(&mut *blocker)
blocker.commit().await?;
assert!(matches!(adding.await?, Err(CmdError::Disabled)));
let keys: i64 = sqlx::query_scalar("SELECT count(*) FROM user_ssh_keys WHERE user_id = $1")
.fetch_one(&pool)
assert_eq!(keys, 0, "the key must not have landed");
async fn disable(pool: &PgPool, user_id: Uuid) -> anyhow::Result<()> {
.bind(user_id)
.execute(pool)
Ok(())
async fn lookup_unknown_fingerprint_returns_none(pool: PgPool) -> anyhow::Result<()> {
let _ = USER.get().unwrap();
.fingerprint("SHA256:ghost-never-registered".to_string())
assert!(lookup.is_none());
/// A public key names one account: a second account cannot claim a key the
/// first already holds, and the first keeps authenticating with it.
async fn add_refuses_a_key_another_account_holds(pool: PgPool) -> anyhow::Result<()> {
let owner = User { id: Uuid::new_v4() };
owner.commit().await?;
// `User::commit` always inserts the same email, which is unique, so a
// second account in one test needs its own.
let other = Uuid::new_v4();
sqlx::query(
"INSERT INTO users (id, user_name, email, photo, verified, user_password, user_role, database_name, created_at)
VALUES ($1, 'Other User', 'other@example.com', 'default.png', TRUE, 'password123', 'user', 'database', NOW())",
)
.bind(other)
.execute(&pool)
.user_id(owner.id)
let claimed = AddSshKey::new()
.user_id(other)
assert!(matches!(claimed, Err(CmdError::Args(_))), "{claimed:?}");
Some(CmdResult::Uuid(id)) if id == owner.id
/// Re-adding your own key must still update its annotation: the insert's
/// `ON CONFLICT (user_id, fingerprint)` has to win over the new unique index.
async fn re_adding_your_own_key_updates_its_annotation(pool: PgPool) -> anyhow::Result<()> {
for annotation in ["first", "second"] {
.key_blob(vec![5])
.annotation(annotation.to_string())