1use anyhow::{anyhow, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4use std::path::Path;
5use std::{fmt, fs};
6
7pub use super::connection::Connection;
8pub use super::User;
9pub use super::{Role, RoleLevelType};
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
48pub struct Config {
49 pub connection: Connection,
50 pub roles: Vec<Role>,
51 pub users: Vec<User>,
52}
53
54impl fmt::Display for Config {
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 write!(f, "{}", serde_yaml::to_string(&self).unwrap())
57 }
58}
59
60impl std::str::FromStr for Config {
61 type Err = anyhow::Error;
62
63 fn from_str(s: &str) -> Result<Self> {
64 let config: Config = serde_yaml::from_str(s)?;
65
66 config.validate()?;
68
69 Ok(config)
70 }
71}
72
73impl Config {
74 pub fn new(config_path: &Path) -> Result<Self> {
75 let config_path = config_path.to_path_buf();
76 let config_str = fs::read_to_string(&config_path).context("failed to read config file")?;
77 let config: Config = serde_yaml::from_str(&config_str)?;
78
79 config.validate()?;
80
81 let config = config.expand_env_vars()?;
83
84 Ok(config)
85 }
86
87 pub fn validate(&self) -> Result<()> {
88 self.connection.validate()?;
90
91 for role in &self.roles {
93 role.validate()?;
94 }
95 let mut role_names = HashSet::new();
97 for role in &self.roles {
98 if role_names.contains(&role.get_name()) {
99 return Err(anyhow!("duplicated role name: {}", role.get_name()));
100 }
101 role_names.insert(role.get_name());
102 }
103
104 for user in &self.users {
106 user.validate()?;
107 }
108 let mut user_names: HashSet<String> = HashSet::new();
110 for user in &self.users {
111 if user_names.contains(&user.name) {
112 return Err(anyhow!("duplicated user: {}", user.name));
113 }
114 user_names.insert(user.name.clone());
115 }
116 for user in &self.users {
118 for role in &user.roles {
119 let role_name = if let Some(without_sign) = role.strip_prefix('-') {
121 without_sign
122 } else {
123 role
124 };
125
126 if !self.roles.iter().any(|r| r.get_name() == role_name) {
127 return Err(anyhow!("user role {} is not available", role));
128 }
129 }
130 }
131
132 Ok(())
133 }
134
135 fn expand_env_vars(&self) -> Result<Self> {
137 let mut config = self.clone();
138
139 config.connection = config.connection.expand_env_vars()?;
141
142 Ok(config)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use indoc::indoc;
150 use std::io::Write;
151 use std::path::PathBuf;
152 use std::str::FromStr;
153 use tempfile::NamedTempFile;
154
155 #[test]
156 #[should_panic(expected = "failed to get content: invalid type: string")]
157 fn test_with_basic_config() {
158 let _text = "bad yaml content";
159 let mut file = NamedTempFile::new().expect("failed to create temp file");
160 file.write(_text.as_bytes())
161 .expect("failed to write to temp file");
162 let path = PathBuf::from(file.path().to_str().unwrap());
163
164 Config::new(&path).expect("failed to get content");
165 }
166
167 #[test]
169 fn test_read_config_basic_config() {
170 let _text = indoc! {"
171 connection:
172 type: postgres
173 url: postgres://localhost:5432/postgres
174 roles: []
175 users: []
176 "};
177
178 let mut file = NamedTempFile::new().expect("failed to create temp file");
179 file.write(_text.as_bytes())
180 .expect("failed to write to temp file");
181 let path = PathBuf::from(file.path().to_str().unwrap());
182
183 Config::new(&path).expect("failed to get content");
184 }
185
186 #[test]
188 fn test_read_config_from_str() {
189 let _text = indoc! {"
190 connection:
191 type: postgres
192 url: postgres://localhost:5432/postgres
193 roles: []
194 users: []
195 "};
196
197 Config::from_str(_text).expect("failed to get content");
198 }
199
200 #[test]
202 fn test_read_config_from_str_and_new() {
203 let _text = indoc! {"
204 connection:
205 type: postgres
206 url: postgres://localhost:5432/postgres
207 roles: []
208 users: []
209 "};
210
211 let config_1 = Config::from_str(_text).expect("failed to get content");
212
213 let mut file = NamedTempFile::new().expect("failed to create temp file");
214 file.write(_text.as_bytes())
215 .expect("failed to write to temp file");
216 let path = PathBuf::from(file.path().to_str().unwrap());
217 let config_2 = Config::new(&path).expect("failed to get content");
218
219 assert_eq!(config_1, config_2);
220 }
221
222 #[test]
224 fn test_read_config_with_env_var() {
225 envmnt::set("POSTGRES_HOST", "duyet");
226
227 let _text = indoc! {"
228 connection:
229 type: postgres
230 url: postgres://${POSTGRES_HOST}:5432/postgres
231 roles: []
232 users: []
233 "};
234
235 let mut file = NamedTempFile::new().expect("failed to create temp file");
236 file.write(_text.as_bytes())
237 .expect("failed to write to temp file");
238 let path = PathBuf::from(file.path().to_str().unwrap());
239
240 let config = Config::new(&path).expect("failed to get content");
241
242 assert_eq!(config.connection.url, "postgres://duyet:5432/postgres");
243
244 envmnt::remove("POSTGRES_HOST");
245 }
246
247 #[test]
249 fn test_read_config_with_env_var_not_available() {
250 let _text = indoc! {"
251 connection:
252 type: postgres
253 url: postgres://${POSTGRES_HOST:duyet}:5432/${POSTGRES_ABC}
254 roles: []
255 users: []
256 "};
257
258 let mut file = NamedTempFile::new().expect("failed to create temp file");
259 file.write(_text.as_bytes())
260 .expect("failed to write to temp file");
261 let path = PathBuf::from(file.path().to_str().unwrap());
262
263 let config = Config::new(&path).expect("failed to get content");
264
265 assert_eq!(
266 config.connection.url,
267 "postgres://duyet:5432/${POSTGRES_ABC}"
268 );
269 }
270
271 #[test]
273 #[should_panic(expected = "connection.type: unknown variant `invalid`")]
274 fn test_read_config_invalid_connection_type() {
275 let _text = indoc! {"
276 connection:
277 type: invalid
278 url: postgres://postgres@localhost:5432/postgres
279 roles: []
280 users: []
281 "};
282
283 let mut file = NamedTempFile::new().expect("failed to create temp file");
284 file.write(_text.as_bytes())
285 .expect("failed to write to temp file");
286 let path = PathBuf::from(file.path().to_str().unwrap());
287
288 Config::new(&path).expect("failed to parse config");
289 }
290
291 #[test]
293 fn test_read_config_one_role_database_level() {
294 let _text = indoc! {"
295 connection:
296 type: postgres
297 url: postgres://localhost:5432/postgres
298 roles:
299 - type: database
300 name: role_database_level_1
301 grants:
302 - CREATE
303 - TEMP
304 databases:
305 - db1
306 - db2
307 - db3
308 - type: database
309 name: role_database_level_2
310 grants:
311 - ALL
312 databases:
313 - db1
314 - db2
315 - db3
316 users: []
317 "};
318
319 let mut file = NamedTempFile::new().expect("failed to create temp file");
320 file.write(_text.as_bytes())
321 .expect("failed to write to temp file");
322 let path = PathBuf::from(file.path().to_str().unwrap());
323
324 let config = Config::new(&path).expect("failed to parse config");
325 assert_eq!(config.roles.len(), 2);
326
327 assert_eq!(config.roles[0].get_name(), "role_database_level_1");
329 assert_eq!(config.roles[0].get_level(), RoleLevelType::Database);
330 assert_eq!(config.roles[0].get_grants().len(), 2);
331 assert_eq!(config.roles[0].get_grants()[0], "CREATE");
332 assert_eq!(config.roles[0].get_grants()[1], "TEMP");
333 assert_eq!(config.roles[0].get_databases().len(), 3);
334 assert_eq!(config.roles[0].get_databases()[0], "db1");
335 assert_eq!(config.roles[0].get_databases()[1], "db2");
336 assert_eq!(config.roles[0].get_databases()[2], "db3");
337 assert_eq!(
338 config.roles[0].to_sql("duyet"),
339 "GRANT CREATE, TEMP ON DATABASE \"db1\", \"db2\", \"db3\" TO \"duyet\";".to_string()
340 );
341
342 assert_eq!(config.roles[1].get_name(), "role_database_level_2");
344 assert_eq!(config.roles[1].get_level(), RoleLevelType::Database);
345 assert_eq!(config.roles[1].get_grants().len(), 1);
346 assert_eq!(config.roles[1].get_grants()[0], "ALL");
347 assert_eq!(config.roles[1].get_databases().len(), 3);
348 assert_eq!(config.roles[1].get_databases()[0], "db1");
349 assert_eq!(config.roles[1].get_databases()[1], "db2");
350 assert_eq!(config.roles[1].get_databases()[2], "db3");
351 assert_eq!(
352 config.roles[1].to_sql("duyet"),
353 "GRANT ALL PRIVILEGES ON DATABASE \"db1\", \"db2\", \"db3\" TO \"duyet\";".to_string()
354 );
355 }
356
357 #[test]
359 #[should_panic(expected = "invalid grant: invalid")]
360 fn test_read_config_role_type_database_level_invalid_grants() {
361 let _text = indoc! {"
362 connection:
363 type: postgres
364 url: postgres://localhost:5432/postgres
365 roles:
366 - type: database
367 name: role_database_level
368 grants:
369 - invalid
370 databases:
371 - db1
372 - db2
373 - db3
374 users: []
375 "};
376
377 let mut file = NamedTempFile::new().expect("failed to create temp file");
378 file.write(_text.as_bytes())
379 .expect("failed to write to temp file");
380 let path = PathBuf::from(file.path().to_str().unwrap());
381
382 Config::new(&path).expect("failed to parse config");
383 }
384
385 #[test]
387 fn test_read_config_one_role_schema_level() {
388 let _text = indoc! {"
389 connection:
390 type: postgres
391 url: postgres://localhost:5432/postgres
392 roles:
393 - type: schema
394 name: role_schema_level_1
395 grants:
396 - CREATE
397 - USAGE
398 schemas:
399 - schema1
400 - schema2
401 - schema3
402 - type: schema
403 name: role_schema_level_2
404 grants:
405 - ALL
406 schemas:
407 - schema1
408 - schema2
409 - schema3
410 users: []
411 "};
412
413 let mut file = NamedTempFile::new().expect("failed to create temp file");
414 file.write(_text.as_bytes())
415 .expect("failed to write to temp file");
416 let path = PathBuf::from(file.path().to_str().unwrap());
417
418 let config = Config::new(&path).expect("failed to parse config");
419 assert_eq!(config.roles.len(), 2);
420
421 assert_eq!(config.roles[0].get_name(), "role_schema_level_1");
423 assert_eq!(config.roles[0].get_level(), RoleLevelType::Schema);
424 assert_eq!(config.roles[0].get_grants().len(), 2);
425 assert_eq!(config.roles[0].get_grants()[0], "CREATE");
426 assert_eq!(config.roles[0].get_grants()[1], "USAGE");
427 assert_eq!(config.roles[0].get_schemas().len(), 3);
428 assert_eq!(config.roles[0].get_schemas()[0], "schema1");
429 assert_eq!(config.roles[0].get_schemas()[1], "schema2");
430 assert_eq!(config.roles[0].get_schemas()[2], "schema3");
431 assert_eq!(
432 config.roles[0].to_sql("duyet"),
433 "GRANT CREATE, USAGE ON SCHEMA \"schema1\", \"schema2\", \"schema3\" TO \"duyet\";"
434 .to_string()
435 );
436
437 assert_eq!(config.roles[1].get_name(), "role_schema_level_2");
439 assert_eq!(config.roles[1].get_level(), RoleLevelType::Schema);
440 assert_eq!(config.roles[1].get_grants().len(), 1);
441 assert_eq!(config.roles[1].get_grants()[0], "ALL");
442 assert_eq!(config.roles[1].get_schemas().len(), 3);
443 assert_eq!(config.roles[1].get_schemas()[0], "schema1");
444 assert_eq!(config.roles[1].get_schemas()[1], "schema2");
445 assert_eq!(config.roles[1].get_schemas()[2], "schema3");
446 assert_eq!(
447 config.roles[1].to_sql("duyet"),
448 "GRANT ALL PRIVILEGES ON SCHEMA \"schema1\", \"schema2\", \"schema3\" TO \"duyet\";"
449 .to_string()
450 );
451 }
452
453 #[test]
455 #[should_panic(expected = "invalid grant: invalid")]
456 fn test_read_config_role_type_schema_level_invalid_grants() {
457 let _text = indoc! {"
458 connection:
459 type: postgres
460 url: postgres://localhost:5432/postgres
461 roles:
462 - type: schema
463 name: role_schema_level
464 grants:
465 - invalid
466 schemas:
467 - schema1
468 - schema2
469 - schema3
470 users: []
471 "};
472
473 let mut file = NamedTempFile::new().expect("failed to create temp file");
474 file.write(_text.as_bytes())
475 .expect("failed to write to temp file");
476 let path = PathBuf::from(file.path().to_str().unwrap());
477
478 Config::new(&path).expect("failed to parse config");
479 }
480
481 #[test]
483 fn test_read_config_one_role_table_level() {
484 let _text = indoc! {"
485 connection:
486 type: postgres
487 url: postgres://localhost:5432/postgres
488 roles:
489 - type: table
490 name: role_table_level_1
491 grants:
492 - SELECT
493 - INSERT
494 schemas:
495 - schema1
496 tables:
497 - table1
498 - table2
499 - table3
500 - type: table
501 name: role_table_level_2
502 grants:
503 - ALL
504 schemas:
505 - schema1
506 tables:
507 - table1
508 - table2
509 - table3
510 users: []
511 "};
512
513 let mut file = NamedTempFile::new().expect("failed to create temp file");
514 file.write(_text.as_bytes())
515 .expect("failed to write to temp file");
516 let path = PathBuf::from(file.path().to_str().unwrap());
517
518 let config = Config::new(&path).expect("failed to parse config");
519 assert_eq!(config.roles.len(), 2);
520
521 assert_eq!(config.roles[0].get_name(), "role_table_level_1");
523 assert_eq!(config.roles[0].get_level(), RoleLevelType::Table);
524 assert_eq!(config.roles[0].get_grants().len(), 2);
525 assert_eq!(config.roles[0].get_grants()[0], "SELECT");
526 assert_eq!(config.roles[0].get_grants()[1], "INSERT");
527 assert_eq!(config.roles[0].get_schemas().len(), 1);
528 assert_eq!(config.roles[0].get_schemas()[0], "schema1");
529 assert_eq!(config.roles[0].get_tables().len(), 3);
530 assert_eq!(config.roles[0].get_tables()[0], "table1");
531 assert_eq!(config.roles[0].get_tables()[1], "table2");
532 assert_eq!(config.roles[0].get_tables()[2], "table3");
533 assert_eq!(
534 config.roles[0].to_sql("duyet"),
535 "GRANT SELECT, INSERT ON \"schema1\".\"table1\", \"schema1\".\"table2\", \"schema1\".\"table3\" TO \"duyet\";"
536 );
537
538 assert_eq!(config.roles[1].get_name(), "role_table_level_2");
540 assert_eq!(config.roles[1].get_level(), RoleLevelType::Table);
541 assert_eq!(config.roles[1].get_grants().len(), 1);
542 assert_eq!(config.roles[1].get_grants()[0], "ALL");
543 assert_eq!(config.roles[1].get_schemas().len(), 1);
544 assert_eq!(config.roles[1].get_schemas()[0], "schema1");
545 assert_eq!(config.roles[1].get_tables().len(), 3);
546 assert_eq!(config.roles[1].get_tables()[0], "table1");
547 assert_eq!(config.roles[1].get_tables()[1], "table2");
548 assert_eq!(config.roles[1].get_tables()[2], "table3");
549 assert_eq!(
550 config.roles[1].to_sql("duyet"),
551 "GRANT ALL PRIVILEGES ON \"schema1\".\"table1\", \"schema1\".\"table2\", \"schema1\".\"table3\" TO \"duyet\";"
552 .to_string()
553 );
554 }
555
556 #[test]
558 fn test_read_config_role_type_table_level_all_tables() {
559 let _text = indoc! {"
560 connection:
561 type: postgres
562 url: postgres://localhost:5432/postgres
563 roles:
564 - type: table
565 name: role_table_level_1
566 grants:
567 - SELECT
568 schemas:
569 - schema1
570 tables:
571 - ALL
572 - type: table
573 name: role_table_level_2
574 grants:
575 - SELECT
576 schemas:
577 - schema1
578 tables:
579 - ALL
580 - another_table_should_be_included_in_all_too
581 - type: table
582 name: role_table_level_3
583 grants:
584 - SELECT
585 schemas:
586 - schema1
587 tables:
588 - ALL
589 - -but_excluded_me
590 - type: table
591 name: role_table_level_4
592 grants:
593 - SELECT
594 schemas:
595 - schema1
596 tables:
597 - table_a
598 - -table_b
599 - type: table
600 name: role_table_level_5
601 grants:
602 - SELECT
603 schemas:
604 - schema1
605 tables:
606 - -table_a
607 - -table_b
608 - type: table
609 name: role_table_level_6
610 grants:
611 - SELECT
612 schemas:
613 - schema1
614 tables:
615 - -ALL
616 users: []
617 "};
618
619 let mut file = NamedTempFile::new().expect("failed to create temp file");
620 file.write(_text.as_bytes())
621 .expect("failed to write to temp file");
622 let path = PathBuf::from(file.path().to_str().unwrap());
623
624 let config = Config::new(&path).expect("failed to parse config");
625 assert_eq!(config.roles.len(), 6);
626
627 assert_eq!(
628 config.roles[0].to_sql("duyet"),
629 "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\";"
630 );
631 assert_eq!(
632 config.roles[1].to_sql("duyet"),
633 "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\";"
634 );
635 assert_eq!(
636 config.roles[2].to_sql("duyet"),
637 "GRANT SELECT ON ALL TABLES IN SCHEMA \"schema1\" TO \"duyet\"; REVOKE SELECT ON \"schema1\".\"but_excluded_me\" FROM \"duyet\";"
638 );
639 assert_eq!(
640 config.roles[3].to_sql("duyet"),
641 "GRANT SELECT ON \"schema1\".\"table_a\" TO \"duyet\"; REVOKE SELECT ON \"schema1\".\"table_b\" FROM \"duyet\";"
642 );
643 assert_eq!(
644 config.roles[4].to_sql("duyet"),
645 "REVOKE SELECT ON \"schema1\".\"table_a\", \"schema1\".\"table_b\" FROM \"duyet\";"
646 );
647 assert_eq!(
648 config.roles[5].to_sql("duyet"),
649 "REVOKE SELECT ON ALL TABLES IN SCHEMA \"schema1\" FROM \"duyet\";"
650 );
651 }
652
653 #[test]
655 #[should_panic(expected = "role.grants invalid")]
656 fn test_read_config_role_type_table_level_invalid_grants() {
657 let _text = indoc! {"
658 connection:
659 type: postgres
660 url: postgres://localhost:5432/postgres
661 roles:
662 - type: table
663 name: role_table_level
664 grants:
665 - invalid
666 schemas:
667 - schema1
668 tables:
669 - table1
670 - table2
671 - table3
672 users: []
673 "};
674
675 let mut file = NamedTempFile::new().expect("failed to create temp file");
676 file.write(_text.as_bytes())
677 .expect("failed to write to temp file");
678 let path = PathBuf::from(file.path().to_str().unwrap());
679
680 Config::new(&path).expect("failed to parse config");
681 }
682
683 #[test]
685 #[should_panic(expected = "duplicated role name: role_table_level")]
686 fn test_read_config_two_role_duplicated_name() {
687 let _text = indoc! {"
688 connection:
689 type: postgres
690 url: postgres://localhost:5432/postgres
691 roles:
692 - type: table
693 name: role_table_level
694 grants:
695 - SELECT
696 - INSERT
697 schemas:
698 - schema1
699 tables:
700 - table1
701 - table2
702 - table3
703 - type: table
704 name: role_table_level
705 grants:
706 - ALL
707 schemas:
708 - schema1
709 tables:
710 - table1
711 - table2
712 - table3
713 users: []
714 "};
715
716 let mut file = NamedTempFile::new().expect("failed to create temp file");
717 file.write(_text.as_bytes())
718 .expect("failed to write to temp file");
719 let path = PathBuf::from(file.path().to_str().unwrap());
720
721 Config::new(&path).expect("failed to parse config");
722 }
723
724 #[test]
726 fn test_read_config_users() {
727 let _text = indoc! {"
728 connection:
729 type: postgres
730 url: postgres://postgres:postgres@localhost:5432/postgres
731 roles:
732 - type: database
733 name: role_database_level
734 grants:
735 - CREATE
736 - TEMP
737 databases:
738 - db1
739 - db2
740 - db3
741 - type: schema
742 name: role_schema_level
743 grants:
744 - ALL
745 schemas:
746 - schema1
747 - schema2
748 - schema3
749 - type: table
750 name: role_table_level
751 grants:
752 - SELECT
753 - INSERT
754 schemas:
755 - schema1
756 tables:
757 - table1
758 - table2
759 - table3
760 users:
761 - name: duyet
762 password: 123456
763 roles:
764 - role_database_level
765 - role_schema_level
766 - role_table_level
767 - name: duyet_without_password
768 roles:
769 - role_database_level
770 - role_schema_level
771 - role_table_level
772 "};
773
774 let mut file = NamedTempFile::new().expect("failed to create temp file");
775 file.write(_text.as_bytes())
776 .expect("failed to write to temp file");
777 let path = PathBuf::from(file.path().to_str().unwrap());
778
779 let config = Config::new(&path).expect("failed to parse config");
780 assert_eq!(config.users.len(), 2);
781
782 assert_eq!(config.users[0].get_name(), "duyet");
784 assert_eq!(config.users[0].get_password(), "123456");
785 assert_eq!(config.users[0].get_roles().len(), 3);
786 assert_eq!(config.users[0].get_roles()[0], "role_database_level");
787 assert_eq!(config.users[0].get_roles()[1], "role_schema_level");
788 assert_eq!(config.users[0].get_roles()[2], "role_table_level");
789
790 assert_eq!(
792 config.users[0].to_sql_create().unwrap(),
793 "CREATE USER duyet WITH PASSWORD '123456';"
794 );
795
796 assert_eq!(
798 config.users[1].to_sql_create().unwrap(),
799 "CREATE USER duyet_without_password;"
800 );
801
802 assert_eq!(
804 config.users[0].to_sql_drop().unwrap(),
805 "DROP USER IF EXISTS duyet;"
806 );
807 }
808
809 #[test]
811 fn test_read_config_users_exclude_role_by_minus_role_name() {
812 let _text = indoc! {"
813 connection:
814 type: postgres
815 url: postgres://postgres:postgres@localhost:5432/postgres
816 roles:
817 - type: database
818 name: role_database_level
819 grants:
820 - CREATE
821 - TEMP
822 databases:
823 - db1
824 - db2
825 - db3
826 - type: schema
827 name: role_schema_level
828 grants:
829 - ALL
830 schemas:
831 - schema1
832 - schema2
833 - schema3
834 - type: table
835 name: role_table_level
836 grants:
837 - SELECT
838 - INSERT
839 schemas:
840 - schema1
841 tables:
842 - table1
843 - table2
844 - table3
845 users:
846 - name: duyet
847 password: 123456
848 roles:
849 - -role_database_level
850 - -role_schema_level
851 - -role_table_level
852 - name: duyet_without_password
853 roles:
854 - role_database_level
855 - role_schema_level
856 - role_table_level
857 "};
858
859 let mut file = NamedTempFile::new().expect("failed to create temp file");
860 file.write(_text.as_bytes())
861 .expect("failed to write to temp file");
862 let path = PathBuf::from(file.path().to_str().unwrap());
863
864 let config = Config::new(&path).expect("failed to parse config");
865 assert_eq!(config.users.len(), 2);
866
867 assert_eq!(config.users[0].get_name(), "duyet");
869 assert_eq!(config.users[0].get_password(), "123456");
870 assert_eq!(config.users[0].get_roles().len(), 3);
871 }
872
873 #[test]
875 fn test_find_role_by_name() {
876 let _text = indoc! {"
877 connection:
878 type: postgres
879 url: postgres://postgres:postgres@localhost:5432/postgres
880 roles:
881 - type: database
882 name: role_database_level
883 grants:
884 - CREATE
885 databases:
886 - db1
887 users: []
888 "};
889
890 let mut file = NamedTempFile::new().expect("failed to create temp file");
891 file.write(_text.as_bytes())
892 .expect("failed to write to temp file");
893 let path = PathBuf::from(file.path().to_str().unwrap());
894
895 let config = Config::new(&path).expect("failed to parse config");
896
897 assert!(config
898 .roles
899 .iter()
900 .find(|r| r.find("role_database_level"))
901 .is_some());
902
903 assert!(config
905 .roles
906 .iter()
907 .find(|r| r.find("-role_database_level"))
908 .is_some());
909 }
910}