notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

migration.rs (24477B)


      1 use enostr::{NoteId, Pubkey};
      2 use nostrdb::Ndb;
      3 use serde::{Deserialize, Deserializer};
      4 use tracing::error;
      5 
      6 use crate::{
      7     accounts::AccountsRoute,
      8     column::{Columns, IntermediaryRoute},
      9     route::Route,
     10     timeline::{kind::ListKind, PubkeySource, Timeline, TimelineId, TimelineKind, TimelineRoute},
     11     ui::add_column::AddColumnRoute,
     12     Result,
     13 };
     14 
     15 use notedeck::{DataPath, DataPathType, Directory};
     16 
     17 pub static COLUMNS_FILE: &str = "columns.json";
     18 
     19 fn columns_json(path: &DataPath) -> Option<String> {
     20     let data_path = path.path(DataPathType::Setting);
     21     Directory::new(data_path)
     22         .get_file(COLUMNS_FILE.to_string())
     23         .ok()
     24 }
     25 
     26 #[derive(Deserialize, Debug, PartialEq)]
     27 enum MigrationTimelineRoute {
     28     Timeline(u32),
     29     Thread(String),
     30     Profile(String),
     31     Reply(String),
     32     Quote(String),
     33 }
     34 
     35 impl MigrationTimelineRoute {
     36     fn timeline_route(self) -> Option<TimelineRoute> {
     37         match self {
     38             MigrationTimelineRoute::Timeline(id) => {
     39                 Some(TimelineRoute::Timeline(TimelineId::new(id)))
     40             }
     41             MigrationTimelineRoute::Thread(note_id_hex) => {
     42                 Some(TimelineRoute::Thread(NoteId::from_hex(&note_id_hex).ok()?))
     43             }
     44             MigrationTimelineRoute::Profile(pubkey_hex) => {
     45                 Some(TimelineRoute::Profile(Pubkey::from_hex(&pubkey_hex).ok()?))
     46             }
     47             MigrationTimelineRoute::Reply(note_id_hex) => {
     48                 Some(TimelineRoute::Reply(NoteId::from_hex(&note_id_hex).ok()?))
     49             }
     50             MigrationTimelineRoute::Quote(note_id_hex) => {
     51                 Some(TimelineRoute::Quote(NoteId::from_hex(&note_id_hex).ok()?))
     52             }
     53         }
     54     }
     55 }
     56 
     57 #[derive(Deserialize, Debug, PartialEq)]
     58 enum MigrationRoute {
     59     Timeline(MigrationTimelineRoute),
     60     Accounts(MigrationAccountsRoute),
     61     Relays,
     62     ComposeNote,
     63     AddColumn(MigrationAddColumnRoute),
     64     Support,
     65 }
     66 
     67 impl MigrationRoute {
     68     fn route(self) -> Option<Route> {
     69         match self {
     70             MigrationRoute::Timeline(migration_timeline_route) => {
     71                 Some(Route::Timeline(migration_timeline_route.timeline_route()?))
     72             }
     73             MigrationRoute::Accounts(migration_accounts_route) => {
     74                 Some(Route::Accounts(migration_accounts_route.accounts_route()))
     75             }
     76             MigrationRoute::Relays => Some(Route::Relays),
     77             MigrationRoute::ComposeNote => Some(Route::ComposeNote),
     78             MigrationRoute::AddColumn(migration_add_column_route) => Some(Route::AddColumn(
     79                 migration_add_column_route.add_column_route(),
     80             )),
     81             MigrationRoute::Support => Some(Route::Support),
     82         }
     83     }
     84 }
     85 
     86 #[derive(Deserialize, Debug, PartialEq)]
     87 enum MigrationAccountsRoute {
     88     Accounts,
     89     AddAccount,
     90 }
     91 
     92 impl MigrationAccountsRoute {
     93     fn accounts_route(self) -> AccountsRoute {
     94         match self {
     95             MigrationAccountsRoute::Accounts => AccountsRoute::Accounts,
     96             MigrationAccountsRoute::AddAccount => AccountsRoute::AddAccount,
     97         }
     98     }
     99 }
    100 
    101 #[derive(Deserialize, Debug, PartialEq)]
    102 enum MigrationAddColumnRoute {
    103     Base,
    104     UndecidedNotification,
    105     ExternalNotification,
    106     Hashtag,
    107 }
    108 
    109 impl MigrationAddColumnRoute {
    110     fn add_column_route(self) -> AddColumnRoute {
    111         match self {
    112             MigrationAddColumnRoute::Base => AddColumnRoute::Base,
    113             MigrationAddColumnRoute::UndecidedNotification => AddColumnRoute::UndecidedNotification,
    114             MigrationAddColumnRoute::ExternalNotification => AddColumnRoute::ExternalNotification,
    115             MigrationAddColumnRoute::Hashtag => AddColumnRoute::Hashtag,
    116         }
    117     }
    118 }
    119 
    120 #[derive(Debug, PartialEq)]
    121 struct MigrationColumn {
    122     routes: Vec<MigrationRoute>,
    123 }
    124 
    125 impl<'de> Deserialize<'de> for MigrationColumn {
    126     fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    127     where
    128         D: Deserializer<'de>,
    129     {
    130         let routes = Vec::<MigrationRoute>::deserialize(deserializer)?;
    131 
    132         Ok(MigrationColumn { routes })
    133     }
    134 }
    135 
    136 #[derive(Deserialize, Debug)]
    137 struct MigrationColumns {
    138     columns: Vec<MigrationColumn>,
    139     timelines: Vec<MigrationTimeline>,
    140 }
    141 
    142 #[derive(Deserialize, Debug, Clone, PartialEq)]
    143 struct MigrationTimeline {
    144     id: u32,
    145     kind: MigrationTimelineKind,
    146 }
    147 
    148 impl MigrationTimeline {
    149     fn into_timeline(self, ndb: &Ndb, deck_user_pubkey: Option<&[u8; 32]>) -> Option<Timeline> {
    150         self.kind
    151             .into_timeline_kind()?
    152             .into_timeline(ndb, deck_user_pubkey)
    153     }
    154 }
    155 
    156 #[derive(Deserialize, Clone, Debug, PartialEq)]
    157 enum MigrationListKind {
    158     Contact(MigrationPubkeySource),
    159 }
    160 
    161 impl MigrationListKind {
    162     fn list_kind(self) -> Option<ListKind> {
    163         match self {
    164             MigrationListKind::Contact(migration_pubkey_source) => {
    165                 Some(ListKind::Contact(migration_pubkey_source.pubkey_source()?))
    166             }
    167         }
    168     }
    169 }
    170 
    171 #[derive(Deserialize, Clone, Debug, PartialEq)]
    172 enum MigrationPubkeySource {
    173     Explicit(String),
    174     DeckAuthor,
    175 }
    176 
    177 impl MigrationPubkeySource {
    178     fn pubkey_source(self) -> Option<PubkeySource> {
    179         match self {
    180             MigrationPubkeySource::Explicit(hex) => {
    181                 Some(PubkeySource::Explicit(Pubkey::from_hex(hex.as_str()).ok()?))
    182             }
    183             MigrationPubkeySource::DeckAuthor => Some(PubkeySource::DeckAuthor),
    184         }
    185     }
    186 }
    187 
    188 #[derive(Deserialize, Clone, Debug, PartialEq)]
    189 enum MigrationTimelineKind {
    190     List(MigrationListKind),
    191     Notifications(MigrationPubkeySource),
    192     Profile(MigrationPubkeySource),
    193     Universe,
    194     Generic,
    195     Hashtag(String),
    196 }
    197 
    198 impl MigrationTimelineKind {
    199     fn into_timeline_kind(self) -> Option<TimelineKind> {
    200         match self {
    201             MigrationTimelineKind::List(migration_list_kind) => {
    202                 Some(TimelineKind::List(migration_list_kind.list_kind()?))
    203             }
    204             MigrationTimelineKind::Notifications(migration_pubkey_source) => Some(
    205                 TimelineKind::Notifications(migration_pubkey_source.pubkey_source()?),
    206             ),
    207             MigrationTimelineKind::Profile(migration_pubkey_source) => Some(TimelineKind::Profile(
    208                 migration_pubkey_source.pubkey_source()?,
    209             )),
    210             MigrationTimelineKind::Universe => Some(TimelineKind::Universe),
    211             MigrationTimelineKind::Generic => Some(TimelineKind::Generic),
    212             MigrationTimelineKind::Hashtag(hashtag) => Some(TimelineKind::Hashtag(hashtag)),
    213         }
    214     }
    215 }
    216 
    217 impl MigrationColumns {
    218     fn into_columns(self, ndb: &Ndb, deck_pubkey: Option<&[u8; 32]>) -> Columns {
    219         let mut columns = Columns::default();
    220 
    221         for column in self.columns {
    222             let mut cur_routes = Vec::new();
    223             for route in column.routes {
    224                 match route {
    225                     MigrationRoute::Timeline(MigrationTimelineRoute::Timeline(timeline_id)) => {
    226                         if let Some(migration_tl) =
    227                             self.timelines.iter().find(|tl| tl.id == timeline_id)
    228                         {
    229                             let tl = migration_tl.clone().into_timeline(ndb, deck_pubkey);
    230                             if let Some(tl) = tl {
    231                                 cur_routes.push(IntermediaryRoute::Timeline(tl));
    232                             } else {
    233                                 error!("Problem deserializing timeline {:?}", migration_tl);
    234                             }
    235                         }
    236                     }
    237                     MigrationRoute::Timeline(MigrationTimelineRoute::Thread(_thread)) => {}
    238                     _ => {
    239                         if let Some(route) = route.route() {
    240                             cur_routes.push(IntermediaryRoute::Route(route));
    241                         }
    242                     }
    243                 }
    244             }
    245             if !cur_routes.is_empty() {
    246                 columns.insert_intermediary_routes(cur_routes);
    247             }
    248         }
    249         columns
    250     }
    251 }
    252 
    253 fn string_to_columns(
    254     serialized_columns: String,
    255     ndb: &Ndb,
    256     user: Option<&[u8; 32]>,
    257 ) -> Option<Columns> {
    258     Some(
    259         deserialize_columns_string(serialized_columns)
    260             .ok()?
    261             .into_columns(ndb, user),
    262     )
    263 }
    264 
    265 pub fn deserialize_columns(path: &DataPath, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<Columns> {
    266     string_to_columns(columns_json(path)?, ndb, user)
    267 }
    268 
    269 fn deserialize_columns_string(serialized_columns: String) -> Result<MigrationColumns> {
    270     Ok(
    271         serde_json::from_str::<MigrationColumns>(&serialized_columns)
    272             .map_err(notedeck::Error::Json)?,
    273     )
    274 }
    275 
    276 #[cfg(test)]
    277 mod tests {
    278     use crate::storage::migration::{
    279         MigrationColumn, MigrationListKind, MigrationPubkeySource, MigrationRoute,
    280         MigrationTimeline, MigrationTimelineKind, MigrationTimelineRoute,
    281     };
    282 
    283     impl MigrationColumn {
    284         fn from_route(route: MigrationRoute) -> Self {
    285             Self {
    286                 routes: vec![route],
    287             }
    288         }
    289 
    290         fn from_routes(routes: Vec<MigrationRoute>) -> Self {
    291             Self { routes }
    292         }
    293     }
    294 
    295     impl MigrationTimeline {
    296         fn new(id: u32, kind: MigrationTimelineKind) -> Self {
    297             Self { id, kind }
    298         }
    299     }
    300 
    301     use super::*;
    302 
    303     #[test]
    304     fn multi_column() {
    305         let route = r#"{"columns":[[{"Timeline":{"Timeline":2}}],[{"Timeline":{"Timeline":0}}],[{"Timeline":{"Timeline":1}}]],"timelines":[{"id":0,"kind":{"List":{"Contact":{"Explicit":"aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"}}}},{"id":1,"kind":{"Hashtag":"introductions"}},{"id":2,"kind":"Universe"}]}"#; // Multi-column
    306 
    307         let deserialized_columns = deserialize_columns_string(route.to_string());
    308         assert!(deserialized_columns.is_ok());
    309 
    310         let migration_cols = deserialized_columns.unwrap();
    311 
    312         assert_eq!(migration_cols.columns.len(), 3);
    313         assert_eq!(
    314             *migration_cols.columns.first().unwrap(),
    315             MigrationColumn::from_route(MigrationRoute::Timeline(
    316                 MigrationTimelineRoute::Timeline(2)
    317             ))
    318         );
    319 
    320         assert_eq!(
    321             *migration_cols.columns.get(1).unwrap(),
    322             MigrationColumn::from_route(MigrationRoute::Timeline(
    323                 MigrationTimelineRoute::Timeline(0)
    324             ))
    325         );
    326 
    327         assert_eq!(
    328             *migration_cols.columns.get(2).unwrap(),
    329             MigrationColumn::from_route(MigrationRoute::Timeline(
    330                 MigrationTimelineRoute::Timeline(1)
    331             ))
    332         );
    333 
    334         assert_eq!(migration_cols.timelines.len(), 3);
    335         assert_eq!(
    336             *migration_cols.timelines.first().unwrap(),
    337             MigrationTimeline::new(
    338                 0,
    339                 MigrationTimelineKind::List(MigrationListKind::Contact(
    340                     MigrationPubkeySource::Explicit(
    341                         "aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"
    342                             .to_owned()
    343                     )
    344                 ))
    345             )
    346         );
    347         assert_eq!(
    348             *migration_cols.timelines.get(1).unwrap(),
    349             MigrationTimeline::new(
    350                 1,
    351                 MigrationTimelineKind::Hashtag("introductions".to_owned())
    352             )
    353         );
    354 
    355         assert_eq!(
    356             *migration_cols.timelines.get(2).unwrap(),
    357             MigrationTimeline::new(2, MigrationTimelineKind::Universe)
    358         )
    359     }
    360 
    361     #[test]
    362     fn base() {
    363         let route = r#"{"columns":[[{"AddColumn":"Base"}]],"timelines":[]}"#;
    364 
    365         let deserialized_columns = deserialize_columns_string(route.to_string());
    366         assert!(deserialized_columns.is_ok());
    367 
    368         let migration_cols = deserialized_columns.unwrap();
    369         assert_eq!(migration_cols.columns.len(), 1);
    370         assert_eq!(
    371             *migration_cols.columns.first().unwrap(),
    372             MigrationColumn::from_route(MigrationRoute::AddColumn(MigrationAddColumnRoute::Base))
    373         );
    374 
    375         assert!(migration_cols.timelines.is_empty());
    376     }
    377 
    378     #[test]
    379     fn universe() {
    380         let route = r#"{"columns":[[{"Timeline":{"Timeline":0}}]],"timelines":[{"id":0,"kind":"Universe"}]}"#;
    381         let deserialized_columns = deserialize_columns_string(route.to_string());
    382         assert!(deserialized_columns.is_ok());
    383 
    384         let migration_cols = deserialized_columns.unwrap();
    385         assert_eq!(migration_cols.columns.len(), 1);
    386         assert_eq!(
    387             *migration_cols.columns.first().unwrap(),
    388             MigrationColumn::from_route(MigrationRoute::Timeline(
    389                 MigrationTimelineRoute::Timeline(0)
    390             ))
    391         );
    392 
    393         assert_eq!(migration_cols.timelines.len(), 1);
    394         assert_eq!(
    395             *migration_cols.timelines.first().unwrap(),
    396             MigrationTimeline::new(0, MigrationTimelineKind::Universe)
    397         )
    398     }
    399 
    400     #[test]
    401     fn home() {
    402         let route = r#"{"columns":[[{"Timeline":{"Timeline":2}}]],"timelines":[{"id":2,"kind":{"List":{"Contact":{"Explicit":"aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"}}}}]}"#;
    403 
    404         let deserialized_columns = deserialize_columns_string(route.to_string());
    405         assert!(deserialized_columns.is_ok());
    406 
    407         let migration_cols = deserialized_columns.unwrap();
    408         assert_eq!(migration_cols.columns.len(), 1);
    409         assert_eq!(
    410             *migration_cols.columns.first().unwrap(),
    411             MigrationColumn::from_route(MigrationRoute::Timeline(
    412                 MigrationTimelineRoute::Timeline(2)
    413             ))
    414         );
    415 
    416         assert_eq!(migration_cols.timelines.len(), 1);
    417         assert_eq!(
    418             *migration_cols.timelines.first().unwrap(),
    419             MigrationTimeline::new(
    420                 2,
    421                 MigrationTimelineKind::List(MigrationListKind::Contact(
    422                     MigrationPubkeySource::Explicit(
    423                         "aa733081e4f0f79dd43023d8983265593f2b41a988671cfcef3f489b91ad93fe"
    424                             .to_owned()
    425                     )
    426                 ))
    427             )
    428         )
    429     }
    430 
    431     #[test]
    432     fn thread() {
    433         let route = r#"{"columns":[[{"Timeline":{"Timeline":7}},{"Timeline":{"Thread":"fb9b0c62bc91bbe28ca428fc85e310ae38795b94fb910e0f4e12962ced971f25"}}]],"timelines":[{"id":7,"kind":{"List":{"Contact":{"Explicit":"4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"}}}}]}"#;
    434 
    435         let deserialized_columns = deserialize_columns_string(route.to_string());
    436         assert!(deserialized_columns.is_ok());
    437 
    438         let migration_cols = deserialized_columns.unwrap();
    439         assert_eq!(migration_cols.columns.len(), 1);
    440         assert_eq!(
    441             *migration_cols.columns.first().unwrap(),
    442             MigrationColumn::from_routes(vec![
    443                 MigrationRoute::Timeline(MigrationTimelineRoute::Timeline(7),),
    444                 MigrationRoute::Timeline(MigrationTimelineRoute::Thread(
    445                     "fb9b0c62bc91bbe28ca428fc85e310ae38795b94fb910e0f4e12962ced971f25".to_owned()
    446                 )),
    447             ])
    448         );
    449 
    450         assert_eq!(migration_cols.timelines.len(), 1);
    451         assert_eq!(
    452             *migration_cols.timelines.first().unwrap(),
    453             MigrationTimeline::new(
    454                 7,
    455                 MigrationTimelineKind::List(MigrationListKind::Contact(
    456                     MigrationPubkeySource::Explicit(
    457                         "4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"
    458                             .to_owned()
    459                     )
    460                 ))
    461             )
    462         )
    463     }
    464 
    465     #[test]
    466     fn profile() {
    467         let route = r#"{"columns":[[{"Timeline":{"Timeline":7}},{"Timeline":{"Profile":"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"}}]],"timelines":[{"id":7,"kind":{"List":{"Contact":{"Explicit":"4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"}}}}]}"#;
    468 
    469         let deserialized_columns = deserialize_columns_string(route.to_string());
    470         assert!(deserialized_columns.is_ok());
    471 
    472         let migration_cols = deserialized_columns.unwrap();
    473         assert_eq!(migration_cols.columns.len(), 1);
    474         assert_eq!(
    475             *migration_cols.columns.first().unwrap(),
    476             MigrationColumn::from_routes(vec![
    477                 MigrationRoute::Timeline(MigrationTimelineRoute::Timeline(7),),
    478                 MigrationRoute::Timeline(MigrationTimelineRoute::Profile(
    479                     "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_owned()
    480                 )),
    481             ])
    482         );
    483 
    484         assert_eq!(migration_cols.timelines.len(), 1);
    485         assert_eq!(
    486             *migration_cols.timelines.first().unwrap(),
    487             MigrationTimeline::new(
    488                 7,
    489                 MigrationTimelineKind::List(MigrationListKind::Contact(
    490                     MigrationPubkeySource::Explicit(
    491                         "4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"
    492                             .to_owned()
    493                     )
    494                 ))
    495             )
    496         )
    497     }
    498 
    499     #[test]
    500     fn your_notifs() {
    501         let route = r#"{"columns":[[{"Timeline":{"Timeline":5}}]],"timelines":[{"id":5,"kind":{"Notifications":"DeckAuthor"}}]}"#;
    502 
    503         let deserialized_columns = deserialize_columns_string(route.to_string());
    504         assert!(deserialized_columns.is_ok());
    505 
    506         let migration_cols = deserialized_columns.unwrap();
    507         assert_eq!(migration_cols.columns.len(), 1);
    508         assert_eq!(
    509             *migration_cols.columns.first().unwrap(),
    510             MigrationColumn::from_route(MigrationRoute::Timeline(
    511                 MigrationTimelineRoute::Timeline(5)
    512             ))
    513         );
    514 
    515         assert_eq!(migration_cols.timelines.len(), 1);
    516         assert_eq!(
    517             *migration_cols.timelines.first().unwrap(),
    518             MigrationTimeline::new(
    519                 5,
    520                 MigrationTimelineKind::Notifications(MigrationPubkeySource::DeckAuthor)
    521             )
    522         )
    523     }
    524 
    525     #[test]
    526     fn undecided_notifs() {
    527         let route = r#"{"columns":[[{"AddColumn":"Base"},{"AddColumn":"UndecidedNotification"}]],"timelines":[]}"#;
    528 
    529         let deserialized_columns = deserialize_columns_string(route.to_string());
    530         assert!(deserialized_columns.is_ok());
    531 
    532         let migration_cols = deserialized_columns.unwrap();
    533         assert_eq!(migration_cols.columns.len(), 1);
    534         assert_eq!(
    535             *migration_cols.columns.first().unwrap(),
    536             MigrationColumn::from_routes(vec![
    537                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    538                 MigrationRoute::AddColumn(MigrationAddColumnRoute::UndecidedNotification),
    539             ])
    540         );
    541 
    542         assert!(migration_cols.timelines.is_empty());
    543     }
    544 
    545     #[test]
    546     fn extern_notifs() {
    547         let route = r#"{"columns":[[{"Timeline":{"Timeline":4}}]],"timelines":[{"id":4,"kind":{"Notifications":{"Explicit":"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"}}}]}"#;
    548 
    549         let deserialized_columns = deserialize_columns_string(route.to_string());
    550         assert!(deserialized_columns.is_ok());
    551 
    552         let migration_cols = deserialized_columns.unwrap();
    553         assert_eq!(migration_cols.columns.len(), 1);
    554         assert_eq!(
    555             *migration_cols.columns.first().unwrap(),
    556             MigrationColumn::from_route(MigrationRoute::Timeline(
    557                 MigrationTimelineRoute::Timeline(4)
    558             ))
    559         );
    560 
    561         assert_eq!(migration_cols.timelines.len(), 1);
    562         assert_eq!(
    563             *migration_cols.timelines.first().unwrap(),
    564             MigrationTimeline::new(
    565                 4,
    566                 MigrationTimelineKind::Notifications(MigrationPubkeySource::Explicit(
    567                     "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245".to_owned()
    568                 ))
    569             )
    570         )
    571     }
    572 
    573     #[test]
    574     fn hashtag() {
    575         let route = r#"{"columns":[[{"Timeline":{"Timeline":6}}]],"timelines":[{"id":6,"kind":{"Hashtag":"notedeck"}}]}"#;
    576 
    577         let deserialized_columns = deserialize_columns_string(route.to_string());
    578         assert!(deserialized_columns.is_ok());
    579 
    580         let migration_cols = deserialized_columns.unwrap();
    581         assert_eq!(migration_cols.columns.len(), 1);
    582         assert_eq!(
    583             *migration_cols.columns.first().unwrap(),
    584             MigrationColumn::from_route(MigrationRoute::Timeline(
    585                 MigrationTimelineRoute::Timeline(6)
    586             ))
    587         );
    588 
    589         assert_eq!(migration_cols.timelines.len(), 1);
    590         assert_eq!(
    591             *migration_cols.timelines.first().unwrap(),
    592             MigrationTimeline::new(6, MigrationTimelineKind::Hashtag("notedeck".to_owned()))
    593         )
    594     }
    595 
    596     #[test]
    597     fn support() {
    598         let route = r#"{"columns":[[{"AddColumn":"Base"},"Support"]],"timelines":[]}"#;
    599 
    600         let deserialized_columns = deserialize_columns_string(route.to_string());
    601         assert!(deserialized_columns.is_ok());
    602 
    603         let migration_cols = deserialized_columns.unwrap();
    604         assert_eq!(migration_cols.columns.len(), 1);
    605         assert_eq!(
    606             *migration_cols.columns.first().unwrap(),
    607             MigrationColumn::from_routes(vec![
    608                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    609                 MigrationRoute::Support
    610             ])
    611         );
    612 
    613         assert!(migration_cols.timelines.is_empty());
    614     }
    615 
    616     #[test]
    617     fn post() {
    618         let route = r#"{"columns":[[{"AddColumn":"Base"},"ComposeNote"]],"timelines":[]}"#;
    619 
    620         let deserialized_columns = deserialize_columns_string(route.to_string());
    621         assert!(deserialized_columns.is_ok());
    622 
    623         let migration_cols = deserialized_columns.unwrap();
    624         assert_eq!(migration_cols.columns.len(), 1);
    625         assert_eq!(
    626             *migration_cols.columns.first().unwrap(),
    627             MigrationColumn::from_routes(vec![
    628                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    629                 MigrationRoute::ComposeNote
    630             ])
    631         );
    632 
    633         assert!(migration_cols.timelines.is_empty());
    634     }
    635 
    636     #[test]
    637     fn relay() {
    638         let route = r#"{"columns":[[{"AddColumn":"Base"},"Relays"]],"timelines":[]}"#;
    639 
    640         let deserialized_columns = deserialize_columns_string(route.to_string());
    641         assert!(deserialized_columns.is_ok());
    642 
    643         let migration_cols = deserialized_columns.unwrap();
    644         assert_eq!(migration_cols.columns.len(), 1);
    645         assert_eq!(
    646             *migration_cols.columns.first().unwrap(),
    647             MigrationColumn::from_routes(vec![
    648                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    649                 MigrationRoute::Relays
    650             ])
    651         );
    652 
    653         assert!(migration_cols.timelines.is_empty());
    654     }
    655 
    656     #[test]
    657     fn accounts() {
    658         let route =
    659             r#"{"columns":[[{"AddColumn":"Base"},{"Accounts":"Accounts"}]],"timelines":[]}"#;
    660 
    661         let deserialized_columns = deserialize_columns_string(route.to_string());
    662         assert!(deserialized_columns.is_ok());
    663 
    664         let migration_cols = deserialized_columns.unwrap();
    665         assert_eq!(migration_cols.columns.len(), 1);
    666         assert_eq!(
    667             *migration_cols.columns.first().unwrap(),
    668             MigrationColumn::from_routes(vec![
    669                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    670                 MigrationRoute::Accounts(MigrationAccountsRoute::Accounts),
    671             ])
    672         );
    673 
    674         assert!(migration_cols.timelines.is_empty());
    675     }
    676 
    677     #[test]
    678     fn login() {
    679         let route = r#"{"columns":[[{"AddColumn":"Base"},{"Accounts":"Accounts"},{"Accounts":"AddAccount"}]],"timelines":[]}"#;
    680 
    681         let deserialized_columns = deserialize_columns_string(route.to_string());
    682         assert!(deserialized_columns.is_ok());
    683 
    684         let migration_cols = deserialized_columns.unwrap();
    685         assert_eq!(migration_cols.columns.len(), 1);
    686         assert_eq!(
    687             *migration_cols.columns.first().unwrap(),
    688             MigrationColumn::from_routes(vec![
    689                 MigrationRoute::AddColumn(MigrationAddColumnRoute::Base),
    690                 MigrationRoute::Accounts(MigrationAccountsRoute::Accounts),
    691                 MigrationRoute::Accounts(MigrationAccountsRoute::AddAccount),
    692             ])
    693         );
    694 
    695         assert!(migration_cols.timelines.is_empty());
    696     }
    697 }