webhook_router/
config.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use std::collections::hash_map::HashMap;
8use std::fs::File;
9use std::io;
10use std::path::Path;
11
12use serde::Deserialize;
13use serde_json::Value;
14use thiserror::Error;
15
16use crate::handler::Handler;
17
18/// Configuration for routing webhooks.
19#[derive(Deserialize, Debug)]
20pub struct Config {
21    #[serde(default)]
22    pub(crate) post_paths: HashMap<String, Handler>,
23    secrets: Option<String>,
24}
25
26#[derive(Debug, Error)]
27pub enum ConfigError {
28    #[error("deserialization error: {}", source)]
29    Deserialize {
30        #[source]
31        source: serde_json::Error,
32    },
33    #[error("failed to read file: {}", source)]
34    Read {
35        #[source]
36        source: io::Error,
37    },
38}
39
40impl Config {
41    /// Read the configuration from a path.
42    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
43        let fin = File::open(path.as_ref()).map_err(|source| {
44            ConfigError::Read {
45                source,
46            }
47        })?;
48        serde_json::from_reader(fin).map_err(|source| {
49            ConfigError::Deserialize {
50                source,
51            }
52        })
53    }
54
55    pub(crate) fn secrets(&self) -> Result<Value, ConfigError> {
56        if let Some(ref path) = self.secrets {
57            let fin = File::open(path).map_err(|source| {
58                ConfigError::Read {
59                    source,
60                }
61            })?;
62            serde_json::from_reader(fin).map_err(|source| {
63                ConfigError::Deserialize {
64                    source,
65                }
66            })
67        } else {
68            Ok(Value::Null)
69        }
70    }
71
72    #[cfg(test)]
73    pub(crate) fn secrets_path(&self) -> Option<&String> {
74        self.secrets.as_ref()
75    }
76}
77
78#[cfg(test)]
79mod test {
80    use std::fs::File;
81    use std::io::Write;
82
83    use serde_json::{json, Value};
84
85    use crate::config::{Config, ConfigError};
86    use crate::test_utils;
87
88    #[test]
89    fn test_config_empty_paths() {
90        let tempdir = test_utils::create_tempdir();
91        let path = test_utils::write_config(tempdir.path(), json!({}));
92        let config = Config::from_path(path).unwrap();
93
94        assert!(config.post_paths.is_empty());
95        assert_eq!(config.secrets, None);
96        assert_eq!(config.secrets().unwrap(), Value::Null);
97    }
98
99    #[test]
100    fn test_config_unreadable() {
101        let path = {
102            let tempdir = test_utils::create_tempdir();
103            test_utils::write_config(tempdir.path(), json!({}))
104        };
105        let err = Config::from_path(path).unwrap_err();
106
107        if let ConfigError::Read {
108            ..
109        } = err
110        {
111            // expected error
112        } else {
113            panic!("unexpected error: {:?}", err);
114        }
115    }
116
117    #[test]
118    fn test_config_unparseable() {
119        let tempdir = test_utils::create_tempdir();
120        let path = tempdir.path().join("config.json");
121        {
122            let mut fout = File::create(&path).unwrap();
123            fout.write_all(b"not json\n").unwrap();
124        }
125        let err = Config::from_path(path).unwrap_err();
126
127        if let ConfigError::Deserialize {
128            ..
129        } = err
130        {
131            // expected error
132        } else {
133            panic!("unexpected error: {:?}", err);
134        }
135    }
136
137    #[test]
138    fn test_secrets_unreadable() {
139        let config = {
140            let tempdir = test_utils::create_tempdir();
141            let (path, _) = test_utils::write_config_secrets(tempdir.path(), json!({}), json!({}));
142            Config::from_path(path).unwrap()
143        };
144        let err = config.secrets().unwrap_err();
145
146        if let ConfigError::Read {
147            ..
148        } = err
149        {
150            // expected error
151        } else {
152            panic!("unexpected error: {:?}", err);
153        }
154    }
155
156    #[test]
157    fn test_secrets_unparseable() {
158        let tempdir = test_utils::create_tempdir();
159        let secrets_path = tempdir.path().join("secrets.json");
160        let path = test_utils::write_config(
161            tempdir.path(),
162            json!({
163                "secrets": secrets_path.to_str().unwrap(),
164            }),
165        );
166        {
167            let mut fout = File::create(&secrets_path).unwrap();
168            fout.write_all(b"not json\n").unwrap();
169        }
170        let config = Config::from_path(path).unwrap();
171        let err = config.secrets().unwrap_err();
172
173        if let ConfigError::Deserialize {
174            ..
175        } = err
176        {
177            // expected error
178        } else {
179            panic!("unexpected error: {:?}", err);
180        }
181    }
182
183    #[test]
184    fn test_config_parse() {
185        let tempdir = test_utils::create_tempdir();
186        let path = test_utils::write_config(
187            tempdir.path(),
188            json!({
189                "post_paths": {
190                    "hostname.example": {
191                        "path": "path",
192                        "filters": [
193                            {
194                                "kind": "blah",
195                            },
196                        ],
197                        "header_name": "X-WebHook-Type",
198                    },
199                },
200            }),
201        );
202        let config = Config::from_path(path).unwrap();
203
204        assert_eq!(config.post_paths.len(), 1);
205    }
206}