diff --git a/CHANGELOG.md b/CHANGELOG.md index a94eb12..bb36a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## UNRELEASED (YYYY-MM-DD) +### Added + +- the `sea_orm` crate feature allows to use `MagnetLink`, `TorrentFile` and `TorrentID` + in sea_orm models +- `TorrentFile`, `DecodedTorrent` and `DecodedInfo` implement `PartialEq` +- `MagnetFile` implements `FromStr`, `TryFrom`, and `Into` +- `MagnetFile` supports serde de/serialization + ## Version 0.4.0 (2025-11-10) This release is focused on stricter parsing of torrents and magnets, and diff --git a/Cargo.toml b/Cargo.toml index 19b7143..20fc6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,11 @@ sha256 = "1.5" rustc-hex = "2.1" serde = { version = "1", features = [ "derive" ] } fluent-uri = { version = "0.4", features = [ "serde" ] } +# For Sea-ORM integration +sea-orm = { version = "2.0.0-rc.28", optional = true } +sea-orm-migration = { version = "2.0.0-rc.28", optional = true } +async-tempfile = { version = "0.7", optional = true } +tokio = { version = "1", optional = true } [dev-dependencies] serde_json = "1" @@ -33,6 +38,8 @@ serde_json = "1" [features] magnet_force_name = [] unknown_tracker_scheme = [] +sea_orm = [ "dep:sea-orm" ] +test_sea_orm = [ "dep:sea-orm", "sea-orm/sqlx-sqlite", "dep:tokio", "tokio/macros", "tokio/rt", "dep:async-tempfile", "dep:sea-orm-migration", "sea-orm-migration/runtime-tokio" ] [[test]] name = "magnet_force_name" @@ -45,3 +52,9 @@ name = "unknown_tracker_scheme" path = "tests/unknown_tracker_scheme.rs" required-features = [ "unknown_tracker_scheme" ] test = true + +[[test]] +name = "sea_orm" +path = "tests/sea_orm.rs" +required-features = [ "sea_orm", "test_sea_orm" ] +test = true \ No newline at end of file diff --git a/src/id.rs b/src/id.rs index 8953e34..5c2dda1 100644 --- a/src/id.rs +++ b/src/id.rs @@ -17,6 +17,7 @@ use crate::{InfoHash, InfoHashError}; /// [`TorrentID::from_infohash`](crate::id::TorrentID::from_infohash) and /// [`InfoHash::id`](crate::hash::InfoHash::id) methods. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "sea_orm", derive(sea_orm::DeriveValueType))] pub struct TorrentID(String); impl TorrentID { diff --git a/src/magnet.rs b/src/magnet.rs index cd2c8b7..016ece6 100644 --- a/src/magnet.rs +++ b/src/magnet.rs @@ -3,6 +3,8 @@ use fluent_uri::{ParseError as UriParseError, Uri}; use crate::{InfoHash, InfoHashError, TorrentID, Tracker, TrackerError}; +use std::str::FromStr; + /// Error occurred during parsing a [`MagnetLink`](crate::magnet::MagnetLink). #[derive(Clone, Debug, PartialEq)] pub enum MagnetLinkError { @@ -118,7 +120,11 @@ impl std::error::Error for MagnetLinkError { /// /// More information is specified in [BEP-0009](https://bittorrent.org/beps/bep_0009.html), and /// even more appears in the wild, as explained [on Wikipedia](https://en.wikipedia.org/wiki/Magnet_URI_scheme). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "sea_orm", derive(sea_orm::DeriveValueType))] +#[cfg_attr(feature = "sea_orm", sea_orm(value_type = "String"))] +#[serde(try_from = "String")] +#[serde(into = "String")] pub struct MagnetLink { /// Only mandatory field for magnet link parsing, unless the /// `magnet_force_name` crate feature is enabled. @@ -322,6 +328,34 @@ impl std::fmt::Display for MagnetLink { } } +impl PartialEq for MagnetLink { + fn eq(&self, other: &Self) -> bool { + self.query == other.query + } +} + +impl FromStr for MagnetLink { + type Err = MagnetLinkError; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl TryFrom for MagnetLink { + type Error = MagnetLinkError; + + fn try_from(s: String) -> Result { + Self::new(&s) + } +} + +impl From for String { + fn from(m: MagnetLink) -> Self { + m.to_string() + } +} + #[cfg(test)] mod tests { use super::*; @@ -561,4 +595,13 @@ mod tests { .collect::>(); assert!(found.is_empty()); } + + #[test] + fn serialization_roundtrip() { + let magnet_url = std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap(); + let magnet = MagnetLink::new(&magnet_url).unwrap(); + let json_url = serde_json::to_string(&magnet_url).unwrap(); + let deserialized_magnet: MagnetLink = serde_json::from_str(&json_url).unwrap(); + assert_eq!(deserialized_magnet, magnet,); + } } diff --git a/src/torrent_file.rs b/src/torrent_file.rs index aca58a8..98afac6 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -1,5 +1,7 @@ use bt_bencode::Value as BencodeValue; use rustc_hex::ToHex; +#[cfg(feature = "sea_orm")] +use sea_orm::prelude::*; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; @@ -85,7 +87,7 @@ impl std::error::Error for TorrentFileError { /// ```ignore /// std::fs::write("export.torrent", &torrent.to_vec()).unwrap(); /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TorrentFile { pub hash: InfoHash, pub name: String, @@ -98,7 +100,7 @@ pub struct TorrentFile { /// In its present form, DecodedTorrent mostly cares about the info dict, but preserves other fields /// as [`BencodeValue`](bt_bencode::BencodeValue) in an `extra` mapping so you can implement /// your own extra parsing. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct DecodedTorrent { /// Main tracker #[serde(skip_serializing_if = "Option::is_none")] @@ -123,20 +125,10 @@ pub struct DecodedTorrent { impl DecodedTorrent { pub fn files(&self) -> Result, TorrentFileError> { - if self.info.files.is_none() { - if self.info.file_tree.is_none() { - // V1 torrent with single file - Ok(vec![TorrentContent { - path: PathBuf::from(&self.info.name), - size: self.info.length.unwrap(), - }]) - } else { - todo!("v2 torrent files"); - } - } else { + if let Some(info_files) = &self.info.files { // V1 torrent with multiple files let mut files: Vec = vec![]; - for file in self.info.files.as_ref().unwrap() { + for file in info_files { // TODO: error let f: UnsafeV1FileContent = bt_bencode::from_value(file.clone()).unwrap(); if let Some(parsed_file) = f.to_torrent_content()? { @@ -146,8 +138,18 @@ impl DecodedTorrent { // Sort files by alphabetical order files.sort(); - Ok(files) + return Ok(files); + } + + if let Some(_info_file_tree) = &self.info.file_tree { + todo!("v2 torrent files"); } + + // V1 torrent with single file + Ok(vec![TorrentContent { + path: PathBuf::from(&self.info.name), + size: self.info.length.unwrap(), + }]) } } @@ -219,7 +221,7 @@ impl UnsafeV1FileContent { /// mapping so you can implement your own extra parsing. // bt_bencode does not support serializing None options and empty HashMaps, so we skip // serialization in those cases. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct DecodedInfo { #[serde(rename = "meta version")] #[serde(skip_serializing_if = "Option::is_none")] @@ -330,6 +332,61 @@ impl TorrentFile { } } +#[cfg(feature = "sea_orm")] +impl From for sea_orm::sea_query::Value { + fn from(t: TorrentFile) -> Self { + Value::Bytes(Some(t.to_vec())) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::TryGetable for TorrentFile { + fn try_get_by( + res: &sea_orm::QueryResult, + index: I, + ) -> Result { + let val: Vec = res.try_get_by(index)?; + TorrentFile::from_slice(&val).map_err(|e| { + sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { + from: "Bytes", + into: "TorrentFile", + source: std::sync::Arc::new(e), + }) + }) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::ValueType for TorrentFile { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Bytes(Some(s)) => { + TorrentFile::from_slice(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) + } + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "TorrentFile".to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::Bytes + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::VarBinary(StringLen::None) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::Nullable for TorrentFile { + fn null() -> sea_orm::sea_query::Value { + sea_orm::sea_query::Value::Bytes(None) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs new file mode 100644 index 0000000..bbb4b6f --- /dev/null +++ b/tests/sea_orm.rs @@ -0,0 +1,566 @@ +use hightorrent::{MagnetLink, TorrentFile, TorrentID}; +use sea_orm::entity::prelude::*; +use sea_orm::*; + +mod id { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: TorrentID, + pub magnet: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} +} + +mod magnet { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: String, + pub magnet: MagnetLink, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} +} + +pub mod mixed_magnet { + use super::*; + use sea_orm_migration::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub torrent_id: TorrentID, + #[sea_orm(unique)] + pub magnet: MagnetLink, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + pub mod m20251115_01_mixed_magnet { + use sea_orm_migration::{prelude::*, schema::*}; + + #[derive(DeriveMigrationName)] + pub struct Migration; + + #[async_trait::async_trait] + impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(MixedMagnet::Table) + .if_not_exists() + .col(pk_auto(MixedMagnet::Id)) + .col(string(MixedMagnet::TorrentID).unique_key()) + .col(string(MixedMagnet::Magnet).unique_key()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MixedMagnet::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum MixedMagnet { + Table, + Id, + TorrentID, + Magnet, + } + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(migration::m20251115_01_mixed_magnet::Migration)] + } + } +} + +pub mod mixed_torrent { + use super::*; + use sea_orm_migration::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_torrent")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub torrent_id: TorrentID, + #[sea_orm(unique)] + pub torrent_file: TorrentFile, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + pub mod m20251115_01_mixed_torrent { + use sea_orm_migration::{prelude::*, schema::*}; + + #[derive(DeriveMigrationName)] + pub struct Migration; + + #[async_trait::async_trait] + impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(MixedTorrent::Table) + .if_not_exists() + .col(pk_auto(MixedTorrent::Id)) + .col(string(MixedTorrent::TorrentID).unique_key()) + .col(var_binary(MixedTorrent::TorrentFile, 0).unique_key()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MixedTorrent::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum MixedTorrent { + Table, + Id, + TorrentID, + TorrentFile, + } + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(migration::m20251115_01_mixed_torrent::Migration)] + } + } +} + +pub mod optional_mixed_magnet { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "optional_mixed_magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: Option, + pub magnet: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + use sea_orm_migration::prelude::*; + + pub mod m20251118_01_optional_mixed_magnet { + use sea_orm_migration::{prelude::*, schema::*}; + + #[derive(DeriveMigrationName)] + pub struct Migration; + + #[async_trait::async_trait] + impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(OptionalMixedMagnet::Table) + .if_not_exists() + .col(pk_auto(OptionalMixedMagnet::Id)) + .col(string_null(OptionalMixedMagnet::TorrentID)) + .col(string_null(OptionalMixedMagnet::Magnet)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(OptionalMixedMagnet::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum OptionalMixedMagnet { + Table, + Id, + TorrentID, + Magnet, + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20251118_01_optional_mixed_magnet::Migration)] + } + } + } +} + +#[test] +fn test_magnet_active_model() { + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); + + let _model = magnet::ActiveModel { + torrent_id: Set(magnet.id().to_string()), + magnet: Set(magnet.clone()), + ..Default::default() + }; +} + +#[test] +fn test_torrentid_active_model() { + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); + + let _model = id::ActiveModel { + torrent_id: Set(magnet.id()), + magnet: Set(magnet.to_string()), + ..Default::default() + }; +} + +#[tokio::test] +async fn test_magnet_real_db() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("mixed_magnet.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + mixed_magnet::Migrator::up(&db, None).await.unwrap(); + + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); + + let model = mixed_magnet::ActiveModel { + torrent_id: Set(magnet.id()), + magnet: Set(magnet.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let magnet2 = MagnetLink::new( + &std::fs::read_to_string("tests/bittorrent-v2-hybrid-test.magnet").unwrap(), + ) + .unwrap(); + + let model2 = mixed_magnet::ActiveModel { + torrent_id: Set(magnet2.id()), + magnet: Set(magnet2.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model_by_id = mixed_magnet::Entity::find_by_id(nonactive_model.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_id.magnet, magnet); + assert_eq!(nonactive_model.magnet, magnet); + + let nonactive_model2 = model2.try_into_model().unwrap(); + let saved_model_by_id2 = mixed_magnet::Entity::find_by_id(nonactive_model2.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id2, nonactive_model2); + assert_eq!(saved_model_by_id2.magnet, magnet2); + assert_eq!(nonactive_model2.magnet, magnet2); + + // Try query by TorrentID + let saved_model_by_torrentid = mixed_magnet::Entity::find() + .filter(mixed_magnet::Column::TorrentId.eq(magnet.id())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrentid.magnet, magnet); + + // Try query by MagnetLink + let saved_model_by_magnet = mixed_magnet::Entity::find() + .filter(mixed_magnet::Column::Magnet.eq(magnet.clone())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_magnet.magnet, magnet); + + // Try listing torrents + let mut found_one = false; + let mut found_two = false; + let list = mixed_magnet::Entity::find().all(&db).await.unwrap(); + println!("{:?}", list); + for entry in list { + if entry.magnet == magnet && entry.torrent_id == magnet.id() { + found_one = true; + continue; + } + + if entry.magnet == magnet2 && entry.torrent_id == magnet2.id() { + found_two = true; + } + } + + assert!(found_one); + assert!(found_two); +} + +#[tokio::test] +async fn test_torrent_real_optional_none() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_none.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + optional_mixed_magnet::migration::Migrator::up(&db, None) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_magnet::ActiveModel { + torrent_id: Set(None), + magnet: Set(None), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_magnet::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_magnet::Entity::find() + .filter(optional_mixed_magnet::Column::Magnet.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.magnet.as_ref(), None); + assert_eq!(nonactive_model.magnet.as_ref(), None); +} + +#[tokio::test] +async fn test_torrent_real_optional_notset() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_magnet_none.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + optional_mixed_magnet::migration::Migrator::up(&db, None) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_magnet::ActiveModel { + torrent_id: NotSet, + magnet: NotSet, + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_magnet::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_magnet::Entity::find() + .filter(optional_mixed_magnet::Column::Magnet.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.magnet.as_ref(), None); + assert_eq!(nonactive_model.magnet.as_ref(), None); +} + +#[tokio::test] +async fn test_torrent_real_db() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("mixed.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + mixed_torrent::Migrator::up(&db, None).await.unwrap(); + + let torrent = + TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-test.torrent").unwrap()) + .unwrap(); + + let model = mixed_torrent::ActiveModel { + torrent_id: Set(torrent.id()), + torrent_file: Set(torrent.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let torrent2 = + TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap()) + .unwrap(); + + let model2 = mixed_torrent::ActiveModel { + torrent_id: Set(torrent2.id()), + torrent_file: Set(torrent2.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model_by_id = mixed_torrent::Entity::find_by_id(nonactive_model.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_id.torrent_file, torrent); + assert_eq!(nonactive_model.torrent_file, torrent); + + let nonactive_model2 = model2.try_into_model().unwrap(); + let saved_model_by_id2 = mixed_torrent::Entity::find_by_id(nonactive_model2.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id2, nonactive_model2); + assert_eq!(saved_model_by_id2.torrent_file, torrent2); + assert_eq!(nonactive_model2.torrent_file, torrent2); + + // Try query by TorrentID + let saved_model_by_torrentid = mixed_torrent::Entity::find() + .filter(mixed_torrent::Column::TorrentId.eq(torrent.id())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrentid.torrent_file, torrent); + + // Try query by TorrentFile + let saved_model_by_torrent = mixed_torrent::Entity::find() + .filter(mixed_torrent::Column::TorrentFile.eq(torrent.clone())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrent.torrent_file, torrent); + + // Try listing torrents + let mut found_one = false; + let mut found_two = false; + let list = mixed_torrent::Entity::find().all(&db).await.unwrap(); + println!("{:?}", list); + for entry in list { + if entry.torrent_file == torrent && entry.torrent_id == torrent.id() { + found_one = true; + continue; + } + + if entry.torrent_file == torrent2 && entry.torrent_id == torrent2.id() { + found_two = true; + } + } + + assert!(found_one); + assert!(found_two); +}