oko 0.1.14

Fully local home security system.
Documentation
use serde::{Deserialize, Serialize};
use sqlx::{Result, SqlitePool};
use time::OffsetDateTime;

use crate::db::CameraPermissionView;

use super::Model;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Camera {
    pub camera_id: i64,
    pub name: String,
    pub ip_address: Option<String>,
    pub last_connected: Option<OffsetDateTime>,
    pub is_active: bool,
}

pub struct Default {
    pub camera_id: i64,
    pub ip_address: Option<String>,
    pub last_connected: Option<OffsetDateTime>,
    pub is_active: bool,
}

impl Model for Camera {
    type Default = Default;
    const DEFAULT: Default = Default {
        camera_id: -1,
        ip_address: None,
        last_connected: None,
        is_active: true,
    };

    async fn create_using_self(&mut self, pool: &SqlitePool) -> Result<()> {
        let result = sqlx::query!(
            r#"
            INSERT INTO cameras (name, ip_address, last_connected, is_active)
            VALUES (?, ?, ?, ?)
            RETURNING camera_id
            "#,
            self.name,
            self.ip_address,
            self.last_connected,
            self.is_active
        )
        .fetch_one(pool)
        .await?;

        self.camera_id = result.camera_id;

        Ok(())
    }

    async fn get_using_id(pool: &SqlitePool, camera_id: i64) -> Result<Self> {
        sqlx::query_as!(
            Camera,
            r#"
            SELECT *
            FROM cameras
            WHERE camera_id = ?
            "#,
            camera_id
        )
        .fetch_one(pool)
        .await
    }

    async fn update_using_self(&self, pool: &SqlitePool) -> Result<()> {
        sqlx::query!(
            r#"
            UPDATE cameras
            SET name = ?, ip_address = ?, last_connected = ?, is_active = ?
            WHERE camera_id = ?
            RETURNING camera_id
            "#,
            self.name,
            self.ip_address,
            self.last_connected,
            self.is_active,
            self.camera_id
        )
        .fetch_one(pool)
        .await?;

        Ok(())
    }

    async fn delete_using_id(pool: &SqlitePool, camera_id: i64) -> Result<()> {
        sqlx::query!(
            r#"
            DELETE
            FROM cameras
            WHERE camera_id = ?
            RETURNING camera_id
            "#,
            camera_id
        )
        .fetch_one(pool)
        .await?;

        Ok(())
    }
}

impl Camera {
    pub async fn list_accessible_to_user(
        db: &SqlitePool,
        user_id: i64,
    ) -> Result<Vec<CameraPermissionView>> {
        sqlx::query_as!(
            CameraPermissionView,
            r#"
            SELECT c.camera_id, c.name as camera_name, c.ip_address, cp.can_view, cp.can_control
            FROM cameras c
            JOIN camera_permissions cp ON c.camera_id = cp.camera_id
            WHERE cp.user_id = ? AND cp.can_view
            "#,
            user_id
        )
        .fetch_all(db)
        .await
    }

    pub async fn get_using_ip(pool: &SqlitePool, ip_address: String) -> Result<Self> {
        sqlx::query_as!(
            Camera,
            r#"
            SELECT *
            FROM cameras
            WHERE ip_address = ?
            "#,
            ip_address
        )
        .fetch_one(pool)
        .await
    }
}

#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
    use super::*;

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras")))]
    async fn create(pool: SqlitePool) -> Result<()> {
        let mut camera = Camera {
            camera_id: Camera::DEFAULT.camera_id,
            name: "Test Camera".to_string(),
            ip_address: Camera::DEFAULT.ip_address,
            last_connected: Camera::DEFAULT.last_connected,
            is_active: Camera::DEFAULT.is_active,
        };

        camera.create_using_self(&pool).await?;

        assert_eq!(camera.camera_id, 3);

        let returned_camera = Camera::get_using_id(&pool, camera.camera_id).await?;

        assert_eq!(returned_camera.name, camera.name);
        assert_eq!(returned_camera.ip_address, camera.ip_address);
        assert_eq!(returned_camera.last_connected, camera.last_connected);
        assert!(returned_camera.is_active);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras")))]
    async fn get(pool: SqlitePool) -> Result<()> {
        let camera_id = 1;

        let returned_camera = Camera::get_using_id(&pool, camera_id).await?;

        assert_eq!(returned_camera.camera_id, camera_id);
        assert_eq!(returned_camera.name, "Front Door");
        assert_eq!(
            returned_camera.ip_address,
            Some("127.0.0.1:40000".to_string())
        );
        assert!(returned_camera.is_active);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras")))]
    async fn update(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
        let old_camera = Camera::get_using_id(&pool, 1).await?;

        let updated_camera = Camera {
            camera_id: old_camera.camera_id,
            name: old_camera.name,
            ip_address: Some("192.168.0.24".to_string()),
            last_connected: Some(time::OffsetDateTime::from_unix_timestamp(1_729_443_378)?),
            is_active: false,
        };

        let updated = updated_camera.update_using_self(&pool).await;

        assert!(updated.is_ok());

        let returned_camera = Camera::get_using_id(&pool, old_camera.camera_id).await?;

        assert_eq!(returned_camera.name, updated_camera.name);
        assert_eq!(returned_camera.ip_address, updated_camera.ip_address);
        assert_eq!(
            returned_camera.last_connected,
            updated_camera.last_connected
        );
        assert!(!returned_camera.is_active);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras")))]
    async fn delete(pool: SqlitePool) -> Result<()> {
        let camera_id = 1;
        let deleted = Camera::delete_using_id(&pool, camera_id).await;

        assert!(deleted.is_ok());

        let returned_camera_result = Camera::get_using_id(&pool, camera_id).await;

        assert!(returned_camera_result.is_err());

        let impossible_deleted = Camera::delete_using_id(&pool, camera_id).await;

        assert!(impossible_deleted.is_err());

        Ok(())
    }

    #[sqlx::test(fixtures(
        path = "../../fixtures",
        scripts("users", "cameras", "camera_permissions")
    ))]
    async fn list_accessible_to_user(pool: SqlitePool) -> Result<()> {
        let returned_cameras = Camera::list_accessible_to_user(&pool, 3).await?;

        assert_eq!(returned_cameras.len(), 1);

        assert_eq!(returned_cameras.first().unwrap().camera_id, 1);
        assert_eq!(returned_cameras.first().unwrap().camera_name, "Front Door");
        assert_eq!(
            returned_cameras.first().unwrap().ip_address,
            Some("127.0.0.1:40000".to_owned())
        );
        assert!(returned_cameras.first().unwrap().can_view);
        assert!(!returned_cameras.first().unwrap().can_control);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras")))]
    async fn get_using_ip(pool: SqlitePool) -> Result<()> {
        let ip_address = "127.0.0.1:40000".to_string();

        let returned_camera = Camera::get_using_ip(&pool, ip_address.clone()).await?;

        assert_eq!(returned_camera.camera_id, 1);
        assert_eq!(returned_camera.name, "Front Door");
        assert_eq!(returned_camera.ip_address, Some(ip_address));
        assert!(returned_camera.is_active);

        Ok(())
    }
}