bairelay 1.1.1

RTSP Relay for Reolink Baichuan cameras
Documentation
use anyhow::{Context, Result};
use bairelay_neolink_core::bc_protocol::CameraDriver;

use super::output::{Outcome, UserInfo};

#[derive(Debug, Clone, Copy)]
pub enum UserType {
	User,
	Administrator,
}

impl UserType {
	fn level(self) -> u8 {
		match self {
			UserType::User => 0,
			UserType::Administrator => 1,
		}
	}
}

#[derive(Debug, Clone)]
pub enum Action {
	List,
	Add {
		name: String,
		password: String,
		user_type: UserType,
	},
	Password {
		name: String,
		password: String,
	},
	Delete {
		name: String,
	},
}

pub async fn run(cam: &dyn CameraDriver, action: Action) -> Result<Outcome> {
	match action {
		Action::List => {
			let list = cam.get_users().await.context("get_users failed")?;
			let users = list
				.user_list
				.unwrap_or_default()
				.into_iter()
				.map(|u| UserInfo {
					name: u.user_name,
					level: u.user_level,
				})
				.collect();
			Ok(Outcome::UsersList { users })
		}
		Action::Add {
			name,
			password,
			user_type,
		} => {
			cam.add_user(name.clone(), password, user_type.level())
				.await
				.context("add_user failed")?;
			Ok(Outcome::UserChanged {
				name,
				action: "added".into(),
			})
		}
		Action::Password { name, password } => {
			cam.modify_user(name.clone(), password)
				.await
				.context("modify_user failed")?;
			Ok(Outcome::UserChanged {
				name,
				action: "password changed for".into(),
			})
		}
		Action::Delete { name } => {
			cam.delete_user(name.clone())
				.await
				.context("delete_user failed")?;
			Ok(Outcome::UserChanged {
				name,
				action: "deleted".into(),
			})
		}
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use bairelay_neolink_core::bc::xml::{User, UserList};
	use bairelay_neolink_core::bc_protocol::{Error, FakeCameraBuilder};

	fn user(name: &str, level: u8) -> User {
		User {
			user_name: name.into(),
			user_level: level,
			user_set_state: "none".into(),
			password: None,
			user_id: None,
			login_state: None,
		}
	}

	#[tokio::test]
	async fn user_type_level_mapping() {
		assert_eq!(UserType::User.level(), 0);
		assert_eq!(UserType::Administrator.level(), 1);
	}

	#[tokio::test]
	async fn users_list_maps_records() {
		let fake = FakeCameraBuilder::new()
			.with_users(|| {
				Ok(UserList {
					user_list: Some(vec![user("admin", 1), user("viewer", 0)]),
					..Default::default()
				})
			})
			.build();
		let outcome = run(&*fake, Action::List).await.unwrap();
		let Outcome::UsersList { users } = outcome else {
			panic!("wrong variant");
		};
		assert_eq!(users.len(), 2);
		assert_eq!(users[0].name, "admin");
		assert_eq!(users[0].level, 1);
		assert_eq!(users[1].name, "viewer");
		assert_eq!(users[1].level, 0);
	}

	#[tokio::test]
	async fn users_list_missing_list_is_empty() {
		let fake = FakeCameraBuilder::new()
			.with_users(|| {
				Ok(UserList {
					user_list: None,
					..Default::default()
				})
			})
			.build();
		let outcome = run(&*fake, Action::List).await.unwrap();
		assert_eq!(outcome, Outcome::UsersList { users: vec![] });
	}

	#[tokio::test]
	async fn users_list_error_propagates() {
		let fake = FakeCameraBuilder::new()
			.with_users(|| Err(Error::Other("no")))
			.build();
		let err = run(&*fake, Action::List).await.unwrap_err();
		assert!(format!("{:#}", err).contains("get_users failed"));
	}

	#[tokio::test]
	async fn users_add_records_call_and_returns_added_action() {
		let fake = FakeCameraBuilder::new().build();
		let outcome = run(
			&*fake,
			Action::Add {
				name: "new".into(),
				password: "pw".into(),
				user_type: UserType::Administrator,
			},
		)
		.await
		.unwrap();
		assert_eq!(
			outcome,
			Outcome::UserChanged {
				name: "new".into(),
				action: "added".into(),
			}
		);
		assert_eq!(
			*fake.calls().add_user.lock().unwrap(),
			vec![("new".to_string(), "pw".to_string(), 1u8)]
		);
	}

	#[tokio::test]
	async fn users_password_records_call() {
		let fake = FakeCameraBuilder::new().build();
		let outcome = run(
			&*fake,
			Action::Password {
				name: "u".into(),
				password: "p".into(),
			},
		)
		.await
		.unwrap();
		assert_eq!(
			outcome,
			Outcome::UserChanged {
				name: "u".into(),
				action: "password changed for".into(),
			}
		);
		assert_eq!(
			*fake.calls().modify_user.lock().unwrap(),
			vec![("u".to_string(), "p".to_string())]
		);
	}

	#[tokio::test]
	async fn users_delete_records_call() {
		let fake = FakeCameraBuilder::new().build();
		let outcome = run(
			&*fake,
			Action::Delete {
				name: "gone".into(),
			},
		)
		.await
		.unwrap();
		assert_eq!(
			outcome,
			Outcome::UserChanged {
				name: "gone".into(),
				action: "deleted".into(),
			}
		);
		assert_eq!(
			*fake.calls().delete_user.lock().unwrap(),
			vec!["gone".to_string()]
		);
	}
}