Skip to main content

slack_rs/oauth/
types.rs

1//! OAuth types and configuration
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum OAuthError {
8    #[error("OAuth configuration error: {0}")]
9    ConfigError(String),
10
11    #[error("Network error: {0}")]
12    NetworkError(String),
13
14    #[error("HTTP error {0}: {1}")]
15    HttpError(u16, String),
16
17    #[error("Parse error: {0}")]
18    ParseError(String),
19
20    #[error("Slack API error: {0}")]
21    SlackError(String),
22
23    #[error("State mismatch: expected {expected}, got {actual}")]
24    StateMismatch { expected: String, actual: String },
25
26    #[error("Callback server error: {0}")]
27    ServerError(String),
28
29    #[error("Browser launch error: {0}")]
30    #[allow(dead_code)]
31    BrowserError(String),
32}
33
34/// OAuth configuration
35#[derive(Debug, Clone)]
36pub struct OAuthConfig {
37    pub client_id: String,
38    pub client_secret: String,
39    pub redirect_uri: String,
40    /// Bot scopes (sent as `scope` parameter in OAuth URL)
41    pub scopes: Vec<String>,
42    /// User scopes (sent as `user_scope` parameter in OAuth URL)
43    pub user_scopes: Vec<String>,
44}
45
46impl OAuthConfig {
47    /// Validate that all required fields are set
48    pub fn validate(&self) -> Result<(), OAuthError> {
49        if self.client_id.is_empty() {
50            return Err(OAuthError::ConfigError("client_id is required".to_string()));
51        }
52        if self.client_secret.is_empty() {
53            return Err(OAuthError::ConfigError(
54                "client_secret is required".to_string(),
55            ));
56        }
57        if self.redirect_uri.is_empty() {
58            return Err(OAuthError::ConfigError(
59                "redirect_uri is required".to_string(),
60            ));
61        }
62        if self.scopes.is_empty() && self.user_scopes.is_empty() {
63            return Err(OAuthError::ConfigError(
64                "at least one of bot scopes or user scopes is required".to_string(),
65            ));
66        }
67        Ok(())
68    }
69}
70
71/// OAuth response from Slack
72#[derive(Debug, Serialize, Deserialize)]
73pub struct OAuthResponse {
74    pub ok: bool,
75    pub access_token: Option<String>,
76    pub token_type: Option<String>,
77    pub scope: Option<String>,
78    pub bot_user_id: Option<String>,
79    pub app_id: Option<String>,
80    pub team: Option<TeamInfo>,
81    pub authed_user: Option<AuthedUser>,
82    pub error: Option<String>,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
86pub struct TeamInfo {
87    pub id: String,
88    pub name: String,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
92pub struct AuthedUser {
93    pub id: String,
94    pub scope: Option<String>,
95    pub access_token: Option<String>,
96    pub token_type: Option<String>,
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_oauth_config_validation_success() {
105        let config = OAuthConfig {
106            client_id: "test_client_id".to_string(),
107            client_secret: "test_secret".to_string(),
108            redirect_uri: "http://localhost:8765/callback".to_string(),
109            scopes: vec!["chat:write".to_string()],
110            user_scopes: vec![],
111        };
112
113        assert!(config.validate().is_ok());
114    }
115
116    #[test]
117    fn test_oauth_config_validation_empty_client_id() {
118        let config = OAuthConfig {
119            client_id: "".to_string(),
120            client_secret: "test_secret".to_string(),
121            redirect_uri: "http://localhost:8765/callback".to_string(),
122            scopes: vec!["chat:write".to_string()],
123            user_scopes: vec![],
124        };
125
126        let result = config.validate();
127        assert!(result.is_err());
128        match result {
129            Err(OAuthError::ConfigError(msg)) => assert!(msg.contains("client_id")),
130            _ => panic!("Expected ConfigError"),
131        }
132    }
133
134    #[test]
135    fn test_oauth_config_validation_empty_client_secret() {
136        let config = OAuthConfig {
137            client_id: "test_client_id".to_string(),
138            client_secret: "".to_string(),
139            redirect_uri: "http://localhost:8765/callback".to_string(),
140            scopes: vec!["chat:write".to_string()],
141            user_scopes: vec![],
142        };
143
144        let result = config.validate();
145        assert!(result.is_err());
146        match result {
147            Err(OAuthError::ConfigError(msg)) => assert!(msg.contains("client_secret")),
148            _ => panic!("Expected ConfigError"),
149        }
150    }
151
152    #[test]
153    fn test_oauth_config_validation_empty_redirect_uri() {
154        let config = OAuthConfig {
155            client_id: "test_client_id".to_string(),
156            client_secret: "test_secret".to_string(),
157            redirect_uri: "".to_string(),
158            scopes: vec!["chat:write".to_string()],
159            user_scopes: vec![],
160        };
161
162        let result = config.validate();
163        assert!(result.is_err());
164        match result {
165            Err(OAuthError::ConfigError(msg)) => assert!(msg.contains("redirect_uri")),
166            _ => panic!("Expected ConfigError"),
167        }
168    }
169
170    #[test]
171    fn test_oauth_config_validation_empty_scopes() {
172        let config = OAuthConfig {
173            client_id: "test_client_id".to_string(),
174            client_secret: "test_secret".to_string(),
175            redirect_uri: "http://localhost:8765/callback".to_string(),
176            scopes: vec![],
177            user_scopes: vec![],
178        };
179
180        let result = config.validate();
181        assert!(result.is_err());
182        match result {
183            Err(OAuthError::ConfigError(msg)) => assert!(msg.contains("scopes")),
184            _ => panic!("Expected ConfigError"),
185        }
186    }
187
188    #[test]
189    fn test_oauth_response_deserialization() {
190        let json = r#"{
191            "ok": true,
192            "access_token": "xoxb-test-token",
193            "token_type": "bot",
194            "scope": "chat:write",
195            "bot_user_id": "U123",
196            "app_id": "A456",
197            "team": {
198                "id": "T789",
199                "name": "Test Team"
200            },
201            "authed_user": {
202                "id": "U012",
203                "scope": "users:read",
204                "access_token": "xoxp-test-token",
205                "token_type": "user"
206            }
207        }"#;
208
209        let response: OAuthResponse = serde_json::from_str(json).unwrap();
210        assert!(response.ok);
211        assert_eq!(response.access_token, Some("xoxb-test-token".to_string()));
212        assert_eq!(response.team.as_ref().unwrap().id, "T789");
213        assert_eq!(response.authed_user.as_ref().unwrap().id, "U012");
214    }
215}