1
//! This module contains datastructures and logic for comparing database privileges,
2
//! generating, validating and reducing diffs between two sets of database privileges.
3

            
4
use super::base::{DatabasePrivilegeRow, db_priv_field_human_readable_name};
5
use crate::core::types::{MySQLDatabase, MySQLUser};
6
use prettytable::Table;
7
use serde::{Deserialize, Serialize};
8
use std::{
9
    collections::{BTreeSet, HashMap, hash_map::Entry},
10
    fmt,
11
};
12

            
13
/// This enum represents a change for a single privilege.
14
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
15
pub enum DatabasePrivilegeChange {
16
    YesToNo,
17
    NoToYes,
18
}
19

            
20
impl DatabasePrivilegeChange {
21
    #[must_use]
22
26
    pub fn new(p1: bool, p2: bool) -> Option<DatabasePrivilegeChange> {
23
26
        match (p1, p2) {
24
4
            (true, false) => Some(DatabasePrivilegeChange::YesToNo),
25
3
            (false, true) => Some(DatabasePrivilegeChange::NoToYes),
26
19
            _ => None,
27
        }
28
26
    }
29
}
30

            
31
/// This struct encapsulates the before and after states of the
32
/// access privileges for a single user on a single database.
33
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Default)]
34
pub struct DatabasePrivilegeRowDiff {
35
    // TODO: don't store the db and user here, let the type be stored in a mapping
36
    pub db: MySQLDatabase,
37
    pub user: MySQLUser,
38
    pub select_priv: Option<DatabasePrivilegeChange>,
39
    pub insert_priv: Option<DatabasePrivilegeChange>,
40
    pub update_priv: Option<DatabasePrivilegeChange>,
41
    pub delete_priv: Option<DatabasePrivilegeChange>,
42
    pub create_priv: Option<DatabasePrivilegeChange>,
43
    pub drop_priv: Option<DatabasePrivilegeChange>,
44
    pub alter_priv: Option<DatabasePrivilegeChange>,
45
    pub index_priv: Option<DatabasePrivilegeChange>,
46
    pub create_tmp_table_priv: Option<DatabasePrivilegeChange>,
47
    pub lock_tables_priv: Option<DatabasePrivilegeChange>,
48
    pub references_priv: Option<DatabasePrivilegeChange>,
49
}
50

            
51
impl DatabasePrivilegeRowDiff {
52
    /// Calculates the difference between two [`DatabasePrivilegeRow`] instances.
53
    #[must_use]
54
2
    pub fn from_rows(
55
2
        row1: &DatabasePrivilegeRow,
56
2
        row2: &DatabasePrivilegeRow,
57
2
    ) -> DatabasePrivilegeRowDiff {
58
2
        debug_assert!(row1.db == row2.db && row1.user == row2.user);
59

            
60
2
        DatabasePrivilegeRowDiff {
61
2
            db: row1.db.clone(),
62
2
            user: row1.user.clone(),
63
2
            select_priv: DatabasePrivilegeChange::new(row1.select_priv, row2.select_priv),
64
2
            insert_priv: DatabasePrivilegeChange::new(row1.insert_priv, row2.insert_priv),
65
2
            update_priv: DatabasePrivilegeChange::new(row1.update_priv, row2.update_priv),
66
2
            delete_priv: DatabasePrivilegeChange::new(row1.delete_priv, row2.delete_priv),
67
2
            create_priv: DatabasePrivilegeChange::new(row1.create_priv, row2.create_priv),
68
2
            drop_priv: DatabasePrivilegeChange::new(row1.drop_priv, row2.drop_priv),
69
2
            alter_priv: DatabasePrivilegeChange::new(row1.alter_priv, row2.alter_priv),
70
2
            index_priv: DatabasePrivilegeChange::new(row1.index_priv, row2.index_priv),
71
2
            create_tmp_table_priv: DatabasePrivilegeChange::new(
72
2
                row1.create_tmp_table_priv,
73
2
                row2.create_tmp_table_priv,
74
2
            ),
75
2
            lock_tables_priv: DatabasePrivilegeChange::new(
76
2
                row1.lock_tables_priv,
77
2
                row2.lock_tables_priv,
78
2
            ),
79
2
            references_priv: DatabasePrivilegeChange::new(
80
2
                row1.references_priv,
81
2
                row2.references_priv,
82
2
            ),
83
2
        }
84
2
    }
85

            
86
    /// Returns true if there are no changes in this diff.
87
    #[must_use]
88
3
    pub fn is_empty(&self) -> bool {
89
3
        self.select_priv.is_none()
90
1
            && self.insert_priv.is_none()
91
1
            && self.update_priv.is_none()
92
1
            && self.delete_priv.is_none()
93
1
            && self.create_priv.is_none()
94
1
            && self.drop_priv.is_none()
95
1
            && self.alter_priv.is_none()
96
1
            && self.index_priv.is_none()
97
1
            && self.create_tmp_table_priv.is_none()
98
1
            && self.lock_tables_priv.is_none()
99
1
            && self.references_priv.is_none()
100
3
    }
101

            
102
    /// Retrieves the privilege change for a given privilege name.
103
    pub fn get_privilege_change_by_name(
104
        &self,
105
        privilege_name: &str,
106
    ) -> anyhow::Result<Option<DatabasePrivilegeChange>> {
107
        match privilege_name {
108
            "select_priv" => Ok(self.select_priv),
109
            "insert_priv" => Ok(self.insert_priv),
110
            "update_priv" => Ok(self.update_priv),
111
            "delete_priv" => Ok(self.delete_priv),
112
            "create_priv" => Ok(self.create_priv),
113
            "drop_priv" => Ok(self.drop_priv),
114
            "alter_priv" => Ok(self.alter_priv),
115
            "index_priv" => Ok(self.index_priv),
116
            "create_tmp_table_priv" => Ok(self.create_tmp_table_priv),
117
            "lock_tables_priv" => Ok(self.lock_tables_priv),
118
            "references_priv" => Ok(self.references_priv),
119
            _ => anyhow::bail!("Unknown privilege name: {privilege_name}"),
120
        }
121
    }
122

            
123
    /// Merges another diff into this one, combining them in a sequential manner.
124
    fn mappend(&mut self, other: &DatabasePrivilegeRowDiff) {
125
        debug_assert!(self.db == other.db && self.user == other.user);
126

            
127
        if other.select_priv.is_some() {
128
            self.select_priv = other.select_priv;
129
        }
130
        if other.insert_priv.is_some() {
131
            self.insert_priv = other.insert_priv;
132
        }
133
        if other.update_priv.is_some() {
134
            self.update_priv = other.update_priv;
135
        }
136
        if other.delete_priv.is_some() {
137
            self.delete_priv = other.delete_priv;
138
        }
139
        if other.create_priv.is_some() {
140
            self.create_priv = other.create_priv;
141
        }
142
        if other.drop_priv.is_some() {
143
            self.drop_priv = other.drop_priv;
144
        }
145
        if other.alter_priv.is_some() {
146
            self.alter_priv = other.alter_priv;
147
        }
148
        if other.index_priv.is_some() {
149
            self.index_priv = other.index_priv;
150
        }
151
        if other.create_tmp_table_priv.is_some() {
152
            self.create_tmp_table_priv = other.create_tmp_table_priv;
153
        }
154
        if other.lock_tables_priv.is_some() {
155
            self.lock_tables_priv = other.lock_tables_priv;
156
        }
157
        if other.references_priv.is_some() {
158
            self.references_priv = other.references_priv;
159
        }
160
    }
161

            
162
    /// Removes any no-op changes from the diff, based on the original privilege row.
163
    fn remove_noops(&mut self, from: &DatabasePrivilegeRow) {
164
        fn new_value(
165
            change: Option<&DatabasePrivilegeChange>,
166
            from_value: bool,
167
        ) -> Option<DatabasePrivilegeChange> {
168
            change.as_ref().and_then(|c| match c {
169
                DatabasePrivilegeChange::YesToNo if from_value => {
170
                    Some(DatabasePrivilegeChange::YesToNo)
171
                }
172
                DatabasePrivilegeChange::NoToYes if !from_value => {
173
                    Some(DatabasePrivilegeChange::NoToYes)
174
                }
175
                _ => None,
176
            })
177
        }
178

            
179
        self.select_priv = new_value(self.select_priv.as_ref(), from.select_priv);
180
        self.insert_priv = new_value(self.insert_priv.as_ref(), from.insert_priv);
181
        self.update_priv = new_value(self.update_priv.as_ref(), from.update_priv);
182
        self.delete_priv = new_value(self.delete_priv.as_ref(), from.delete_priv);
183
        self.create_priv = new_value(self.create_priv.as_ref(), from.create_priv);
184
        self.drop_priv = new_value(self.drop_priv.as_ref(), from.drop_priv);
185
        self.alter_priv = new_value(self.alter_priv.as_ref(), from.alter_priv);
186
        self.index_priv = new_value(self.index_priv.as_ref(), from.index_priv);
187
        self.create_tmp_table_priv = new_value(
188
            self.create_tmp_table_priv.as_ref(),
189
            from.create_tmp_table_priv,
190
        );
191
        self.lock_tables_priv = new_value(self.lock_tables_priv.as_ref(), from.lock_tables_priv);
192
        self.references_priv = new_value(self.references_priv.as_ref(), from.references_priv);
193
    }
194

            
195
    fn apply(&self, base: &mut DatabasePrivilegeRow) {
196
        fn apply_change(change: Option<&DatabasePrivilegeChange>, target: &mut bool) {
197
            match change {
198
                Some(DatabasePrivilegeChange::YesToNo) => *target = false,
199
                Some(DatabasePrivilegeChange::NoToYes) => *target = true,
200
                None => {}
201
            }
202
        }
203

            
204
        apply_change(self.select_priv.as_ref(), &mut base.select_priv);
205
        apply_change(self.insert_priv.as_ref(), &mut base.insert_priv);
206
        apply_change(self.update_priv.as_ref(), &mut base.update_priv);
207
        apply_change(self.delete_priv.as_ref(), &mut base.delete_priv);
208
        apply_change(self.create_priv.as_ref(), &mut base.create_priv);
209
        apply_change(self.drop_priv.as_ref(), &mut base.drop_priv);
210
        apply_change(self.alter_priv.as_ref(), &mut base.alter_priv);
211
        apply_change(self.index_priv.as_ref(), &mut base.index_priv);
212
        apply_change(
213
            self.create_tmp_table_priv.as_ref(),
214
            &mut base.create_tmp_table_priv,
215
        );
216
        apply_change(self.lock_tables_priv.as_ref(), &mut base.lock_tables_priv);
217
        apply_change(self.references_priv.as_ref(), &mut base.references_priv);
218
    }
219
}
220

            
221
impl fmt::Display for DatabasePrivilegeRowDiff {
222
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223
        fn format_change(
224
            f: &mut fmt::Formatter<'_>,
225
            change: Option<DatabasePrivilegeChange>,
226
            field_name: &str,
227
        ) -> fmt::Result {
228
            if let Some(change) = change {
229
                match change {
230
                    DatabasePrivilegeChange::YesToNo => f.write_fmt(format_args!(
231
                        "{}: Y -> N\n",
232
                        db_priv_field_human_readable_name(field_name)
233
                    )),
234
                    DatabasePrivilegeChange::NoToYes => f.write_fmt(format_args!(
235
                        "{}: N -> Y\n",
236
                        db_priv_field_human_readable_name(field_name)
237
                    )),
238
                }
239
            } else {
240
                Ok(())
241
            }
242
        }
243

            
244
        format_change(f, self.select_priv, "select_priv")?;
245
        format_change(f, self.insert_priv, "insert_priv")?;
246
        format_change(f, self.update_priv, "update_priv")?;
247
        format_change(f, self.delete_priv, "delete_priv")?;
248
        format_change(f, self.create_priv, "create_priv")?;
249
        format_change(f, self.drop_priv, "drop_priv")?;
250
        format_change(f, self.alter_priv, "alter_priv")?;
251
        format_change(f, self.index_priv, "index_priv")?;
252
        format_change(f, self.create_tmp_table_priv, "create_tmp_table_priv")?;
253
        format_change(f, self.lock_tables_priv, "lock_tables_priv")?;
254
        format_change(f, self.references_priv, "references_priv")?;
255

            
256
        Ok(())
257
    }
258
}
259

            
260
/// This enum encapsulates whether a [`DatabasePrivilegeRow`] was introduced, modified or deleted.
261
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
262
pub enum DatabasePrivilegesDiff {
263
    New(DatabasePrivilegeRow),
264
    Modified(DatabasePrivilegeRowDiff),
265
    Deleted(DatabasePrivilegeRow),
266
    Noop { db: MySQLDatabase, user: MySQLUser },
267
}
268

            
269
impl DatabasePrivilegesDiff {
270
    #[must_use]
271
    pub fn get_database_name(&self) -> &MySQLDatabase {
272
        match self {
273
            DatabasePrivilegesDiff::New(p) => &p.db,
274
            DatabasePrivilegesDiff::Modified(p) => &p.db,
275
            DatabasePrivilegesDiff::Deleted(p) => &p.db,
276
            DatabasePrivilegesDiff::Noop { db, .. } => db,
277
        }
278
    }
279

            
280
    #[must_use]
281
    pub fn get_user_name(&self) -> &MySQLUser {
282
        match self {
283
            DatabasePrivilegesDiff::New(p) => &p.user,
284
            DatabasePrivilegesDiff::Modified(p) => &p.user,
285
            DatabasePrivilegesDiff::Deleted(p) => &p.user,
286
            DatabasePrivilegesDiff::Noop { user, .. } => user,
287
        }
288
    }
289

            
290
    /// Merges another [`DatabasePrivilegesDiff`] into this one, combining them in a sequential manner.
291
    /// For example, if this diff represents a creation and the other represents a modification,
292
    /// the result will be a creation with the modifications applied.
293
    pub fn mappend(&mut self, other: &DatabasePrivilegesDiff) -> anyhow::Result<()> {
294
        debug_assert!(
295
            self.get_database_name() == other.get_database_name()
296
                && self.get_user_name() == other.get_user_name()
297
        );
298

            
299
        if matches!(self, DatabasePrivilegesDiff::Deleted(_))
300
            && (matches!(other, DatabasePrivilegesDiff::Modified(_)))
301
        {
302
            anyhow::bail!("Cannot modify a deleted database privilege row");
303
        }
304

            
305
        if matches!(self, DatabasePrivilegesDiff::New(_))
306
            && (matches!(other, DatabasePrivilegesDiff::New(_)))
307
        {
308
            anyhow::bail!("Cannot create an already existing database privilege row");
309
        }
310

            
311
        if matches!(self, DatabasePrivilegesDiff::Modified(_))
312
            && (matches!(other, DatabasePrivilegesDiff::New(_)))
313
        {
314
            anyhow::bail!("Cannot create an already existing database privilege row");
315
        }
316

            
317
        if matches!(self, DatabasePrivilegesDiff::Noop { .. }) {
318
            other.clone_into(self);
319
            return Ok(());
320
        } else if matches!(other, DatabasePrivilegesDiff::Noop { .. }) {
321
            return Ok(());
322
        }
323

            
324
        match (&self, other) {
325
            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Modified(modified)) => {
326
                let inner_row = match self {
327
                    DatabasePrivilegesDiff::New(r) => r,
328
                    _ => unreachable!(),
329
                };
330
                modified.apply(inner_row);
331
            }
332
            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Modified(modified)) => {
333
                let inner_diff = match self {
334
                    DatabasePrivilegesDiff::Modified(r) => r,
335
                    _ => unreachable!(),
336
                };
337
                inner_diff.mappend(modified);
338

            
339
                if inner_diff.is_empty() {
340
                    let db = inner_diff.db.clone();
341
                    let user = inner_diff.user.clone();
342
                    *self = DatabasePrivilegesDiff::Noop { db, user };
343
                }
344
            }
345
            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Deleted(deleted)) => {
346
                *self = DatabasePrivilegesDiff::Deleted(deleted.to_owned());
347
            }
348
            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Deleted(_)) => {
349
                let db = self.get_database_name().to_owned();
350
                let user = self.get_user_name().to_owned();
351
                *self = DatabasePrivilegesDiff::Noop { db, user };
352
            }
353
            _ => {}
354
        }
355

            
356
        Ok(())
357
    }
358
}
359

            
360
pub type DatabasePrivilegeState<'a> = &'a [DatabasePrivilegeRow];
361

            
362
/// This function calculates the differences between two sets of database privileges.
363
/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
364
/// apply a set of privilege modifications to the database.
365
#[must_use]
366
1
pub fn diff_privileges(
367
1
    from: DatabasePrivilegeState<'_>,
368
1
    to: &[DatabasePrivilegeRow],
369
1
) -> BTreeSet<DatabasePrivilegesDiff> {
370
1
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
371
1
        .iter()
372
1
        .cloned()
373
2
        .map(|p| ((p.db.clone(), p.user.clone()), p))
374
1
        .collect();
375

            
376
1
    let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = to
377
1
        .iter()
378
1
        .cloned()
379
2
        .map(|p| ((p.db.clone(), p.user.clone()), p))
380
1
        .collect();
381

            
382
1
    let mut result = BTreeSet::new();
383

            
384
2
    for p in to {
385
2
        if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) {
386
1
            let diff = DatabasePrivilegeRowDiff::from_rows(old_p, p);
387
1
            if !diff.is_empty() {
388
1
                result.insert(DatabasePrivilegesDiff::Modified(diff));
389
1
            }
390
1
        } else {
391
1
            result.insert(DatabasePrivilegesDiff::New(p.to_owned()));
392
1
        }
393
    }
394

            
395
2
    for p in from {
396
2
        if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) {
397
1
            result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
398
1
        }
399
    }
400

            
401
1
    result
402
1
}
403

            
404
/// Converts a set of [`DatabasePrivilegeRowDiff`] into a set of [`DatabasePrivilegesDiff`],
405
/// representing either creating new privilege rows, or modifying the existing ones.
406
///
407
/// This is particularly useful for processing CLI arguments.
408
pub fn create_or_modify_privilege_rows(
409
    from: DatabasePrivilegeState<'_>,
410
    to: &BTreeSet<DatabasePrivilegeRowDiff>,
411
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
412
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
413
        .iter()
414
        .cloned()
415
        .map(|p| ((p.db.clone(), p.user.clone()), p))
416
        .collect();
417

            
418
    let mut result = BTreeSet::new();
419

            
420
    for diff in to {
421
        if let Some(old_p) = from_lookup_table.get(&(diff.db.clone(), diff.user.clone())) {
422
            let mut modified_diff = diff.to_owned();
423
            modified_diff.remove_noops(old_p);
424
            if !modified_diff.is_empty() {
425
                result.insert(DatabasePrivilegesDiff::Modified(modified_diff));
426
            }
427
        } else {
428
            let mut new_row = DatabasePrivilegeRow {
429
                db: diff.db.clone(),
430
                user: diff.user.clone(),
431
                select_priv: false,
432
                insert_priv: false,
433
                update_priv: false,
434
                delete_priv: false,
435
                create_priv: false,
436
                drop_priv: false,
437
                alter_priv: false,
438
                index_priv: false,
439
                create_tmp_table_priv: false,
440
                lock_tables_priv: false,
441
                references_priv: false,
442
            };
443
            diff.apply(&mut new_row);
444
            result.insert(DatabasePrivilegesDiff::New(new_row));
445
        }
446
    }
447

            
448
    Ok(result)
449
}
450

            
451
/// Reduces a set of [`DatabasePrivilegesDiff`] by removing any modifications that would be no-ops.
452
/// For example, if a privilege is changed from Yes to No, but it was already No, that change
453
/// is removed from the diff.
454
///
455
/// The `from` parameter is used to determine the current state of the privileges.
456
/// The `to` parameter is the set of diffs to be reduced.
457
pub fn reduce_privilege_diffs(
458
    from: DatabasePrivilegeState<'_>,
459
    to: BTreeSet<DatabasePrivilegesDiff>,
460
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
461
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
462
        .iter()
463
        .cloned()
464
        .map(|p| ((p.db.clone(), p.user.clone()), p))
465
        .collect();
466

            
467
    let mut result: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegesDiff> = from_lookup_table
468
        .iter()
469
        .map(|((db, user), _)| {
470
            (
471
                (db.to_owned(), user.to_owned()),
472
                DatabasePrivilegesDiff::Noop {
473
                    db: db.to_owned(),
474
                    user: user.to_owned(),
475
                },
476
            )
477
        })
478
        .collect();
479

            
480
    for diff in to {
481
        let entry = result.entry((
482
            diff.get_database_name().to_owned(),
483
            diff.get_user_name().to_owned(),
484
        ));
485
        match entry {
486
            Entry::Occupied(mut occupied_entry) => {
487
                let existing_diff = occupied_entry.get_mut();
488
                existing_diff.mappend(&diff)?;
489
            }
490
            Entry::Vacant(vacant_entry) => {
491
                vacant_entry.insert(diff.clone());
492
            }
493
        }
494
    }
495

            
496
    for (key, diff) in &mut result {
497
        if let Some(from_row) = from_lookup_table.get(key)
498
            && let DatabasePrivilegesDiff::Modified(modified_diff) = diff
499
        {
500
            modified_diff.remove_noops(from_row);
501
            if modified_diff.is_empty() {
502
                let db = modified_diff.db.clone();
503
                let user = modified_diff.user.clone();
504
                *diff = DatabasePrivilegesDiff::Noop { db, user };
505
            }
506
        }
507
    }
508

            
509
    Ok(result
510
        .into_values()
511
        .filter(|diff| !matches!(diff, DatabasePrivilegesDiff::Noop { .. }))
512
        .collect::<BTreeSet<DatabasePrivilegesDiff>>())
513
}
514

            
515
/// Renders a set of [`DatabasePrivilegesDiff`] into a human-readable formatted table.
516
#[must_use]
517
pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
518
    let mut table = Table::new();
519
    table.set_titles(row!["Database", "User", "Privilege diff",]);
520
    for row in diffs {
521
        match row {
522
            DatabasePrivilegesDiff::New(p) => {
523
                table.add_row(row![
524
                    p.db,
525
                    p.user,
526
                    "(Previously unprivileged)\n".to_string() + &p.to_string()
527
                ]);
528
            }
529
            DatabasePrivilegesDiff::Modified(p) => {
530
                table.add_row(row![p.db, p.user, p.to_string(),]);
531
            }
532
            DatabasePrivilegesDiff::Deleted(p) => {
533
                table.add_row(row![p.db, p.user, "Removed".to_string()]);
534
            }
535
            DatabasePrivilegesDiff::Noop { db, user } => {
536
                table.add_row(row![db, user, "No changes".to_string()]);
537
            }
538
        }
539
    }
540

            
541
    table.to_string()
542
}
543

            
544
#[cfg(test)]
545
mod tests {
546
    use super::*;
547

            
548
    #[test]
549
1
    fn test_database_privilege_change_creation() {
550
1
        assert_eq!(
551
1
            DatabasePrivilegeChange::new(true, false),
552
            Some(DatabasePrivilegeChange::YesToNo),
553
        );
554
1
        assert_eq!(
555
1
            DatabasePrivilegeChange::new(false, true),
556
            Some(DatabasePrivilegeChange::NoToYes),
557
        );
558
1
        assert_eq!(DatabasePrivilegeChange::new(true, true), None);
559
1
        assert_eq!(DatabasePrivilegeChange::new(false, false), None);
560
1
    }
561

            
562
    #[test]
563
1
    fn test_database_privilege_row_diff_from_rows() {
564
1
        let row1 = DatabasePrivilegeRow {
565
1
            db: "db".into(),
566
1
            user: "user".into(),
567
1

            
568
1
            select_priv: true,
569
1
            insert_priv: false,
570
1
            update_priv: true,
571
1
            delete_priv: false,
572
1

            
573
1
            create_priv: false,
574
1
            drop_priv: false,
575
1
            alter_priv: false,
576
1
            index_priv: false,
577
1
            create_tmp_table_priv: false,
578
1
            lock_tables_priv: false,
579
1
            references_priv: false,
580
1
        };
581
1
        let row2 = DatabasePrivilegeRow {
582
1
            db: "db".into(),
583
1
            user: "user".into(),
584
1

            
585
1
            select_priv: true,
586
1
            insert_priv: true,
587
1
            update_priv: false,
588
1
            delete_priv: false,
589
1

            
590
1
            create_priv: false,
591
1
            drop_priv: false,
592
1
            alter_priv: false,
593
1
            index_priv: false,
594
1
            create_tmp_table_priv: false,
595
1
            lock_tables_priv: false,
596
1
            references_priv: false,
597
1
        };
598

            
599
1
        let diff = DatabasePrivilegeRowDiff::from_rows(&row1, &row2);
600
1
        assert_eq!(
601
            diff,
602
1
            DatabasePrivilegeRowDiff {
603
1
                db: "db".into(),
604
1
                user: "user".into(),
605
1
                select_priv: None,
606
1
                insert_priv: Some(DatabasePrivilegeChange::NoToYes),
607
1
                update_priv: Some(DatabasePrivilegeChange::YesToNo),
608
1
                delete_priv: None,
609
1
                ..Default::default()
610
1
            },
611
        );
612
1
    }
613

            
614
    #[test]
615
1
    fn test_database_privilege_row_diff_is_empty() {
616
1
        let empty_diff = DatabasePrivilegeRowDiff {
617
1
            db: "db".into(),
618
1
            user: "user".into(),
619
1
            ..Default::default()
620
1
        };
621

            
622
1
        assert!(empty_diff.is_empty());
623

            
624
1
        let non_empty_diff = DatabasePrivilegeRowDiff {
625
1
            db: "db".into(),
626
1
            user: "user".into(),
627
1
            select_priv: Some(DatabasePrivilegeChange::YesToNo),
628
1
            ..Default::default()
629
1
        };
630

            
631
1
        assert!(!non_empty_diff.is_empty());
632
1
    }
633

            
634
    // TODO: test in isolation:
635
    // DatabasePrivilegeRowDiff::mappend
636
    // DatabasePrivilegeRowDiff::remove_noops
637
    // DatabasePrivilegeRowDiff::apply
638
    //
639
    // DatabasePrivilegesDiff::mappend
640
    //
641
    // reduce_privilege_diffs
642

            
643
    #[test]
644
1
    fn test_diff_privileges() {
645
1
        let row_to_be_modified = DatabasePrivilegeRow {
646
1
            db: "db".into(),
647
1
            user: "user".into(),
648
1
            select_priv: true,
649
1
            insert_priv: true,
650
1
            update_priv: true,
651
1
            delete_priv: true,
652
1
            create_priv: true,
653
1
            drop_priv: true,
654
1
            alter_priv: true,
655
1
            index_priv: false,
656
1
            create_tmp_table_priv: true,
657
1
            lock_tables_priv: true,
658
1
            references_priv: false,
659
1
        };
660

            
661
1
        let mut row_to_be_deleted = row_to_be_modified.to_owned();
662
1
        "user2".clone_into(&mut row_to_be_deleted.user);
663

            
664
1
        let from = vec![row_to_be_modified.to_owned(), row_to_be_deleted.to_owned()];
665

            
666
1
        let mut modified_row = row_to_be_modified.to_owned();
667
1
        modified_row.select_priv = false;
668
1
        modified_row.insert_priv = false;
669
1
        modified_row.index_priv = true;
670

            
671
1
        let mut new_row = row_to_be_modified.to_owned();
672
1
        "user3".clone_into(&mut new_row.user);
673

            
674
1
        let to = vec![modified_row.to_owned(), new_row.to_owned()];
675

            
676
1
        let diffs = diff_privileges(&from, &to);
677

            
678
1
        assert_eq!(
679
            diffs,
680
1
            BTreeSet::from_iter(vec![
681
1
                DatabasePrivilegesDiff::Deleted(row_to_be_deleted),
682
1
                DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
683
1
                    db: "db".into(),
684
1
                    user: "user".into(),
685
1
                    select_priv: Some(DatabasePrivilegeChange::YesToNo),
686
1
                    insert_priv: Some(DatabasePrivilegeChange::YesToNo),
687
1
                    index_priv: Some(DatabasePrivilegeChange::NoToYes),
688
1
                    ..Default::default()
689
1
                }),
690
1
                DatabasePrivilegesDiff::New(new_row),
691
            ])
692
        );
693
1
    }
694
}