1use thiserror::Error;
6
7use super::client::HttpClient;
8use crate::constants::SPOTIFY_AUTH_BASE_URL;
9
10#[derive(Debug, Error)]
12pub enum AuthError {
13 #[error("Request failed: {0}")]
14 Request(#[from] reqwest::Error),
15
16 #[error("Token exchange failed ({status}): {message}")]
17 TokenExchange { status: u16, message: String },
18}
19
20pub struct SpotifyAuth {
24 http: HttpClient,
25 base_url: String,
26}
27
28impl SpotifyAuth {
29 pub fn new() -> Self {
31 Self {
32 http: HttpClient::new(),
33 base_url: SPOTIFY_AUTH_BASE_URL.to_string(),
34 }
35 }
36
37 pub fn with_base_url(base_url: String) -> Self {
41 Self {
42 http: HttpClient::new(),
43 base_url,
44 }
45 }
46
47 pub fn url(path: &str) -> String {
49 format!("{}{}", SPOTIFY_AUTH_BASE_URL, path)
50 }
51
52 fn endpoint(&self, path: &str) -> String {
54 format!("{}{}", self.base_url, path)
55 }
56
57 pub async fn exchange_code(
59 &self,
60 client_id: &str,
61 code: &str,
62 redirect_uri: &str,
63 code_verifier: &str,
64 ) -> Result<serde_json::Value, AuthError> {
65 let params = [
66 ("grant_type", "authorization_code"),
67 ("code", code),
68 ("redirect_uri", redirect_uri),
69 ("client_id", client_id),
70 ("code_verifier", code_verifier),
71 ];
72
73 self.token_request(¶ms).await
74 }
75
76 pub async fn refresh_token(
78 &self,
79 client_id: &str,
80 refresh_token: &str,
81 ) -> Result<serde_json::Value, AuthError> {
82 let params = [
83 ("grant_type", "refresh_token"),
84 ("refresh_token", refresh_token),
85 ("client_id", client_id),
86 ];
87
88 self.token_request(¶ms).await
89 }
90
91 async fn token_request(&self, params: &[(&str, &str)]) -> Result<serde_json::Value, AuthError> {
92 let response = self
93 .http
94 .inner()
95 .post(self.endpoint("/api/token"))
96 .form(params)
97 .send()
98 .await?;
99
100 if !response.status().is_success() {
101 let status = response.status();
102 let body = response.text().await.unwrap_or_default();
103 return Err(AuthError::TokenExchange {
104 status: status.as_u16(),
105 message: body,
106 });
107 }
108
109 let json: serde_json::Value = response.json().await?;
110 Ok(json)
111 }
112}
113
114impl Default for SpotifyAuth {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn auth_error_display() {
126 let err = AuthError::TokenExchange {
127 status: 400,
128 message: "invalid_grant".to_string(),
129 };
130 let display = format!("{}", err);
131 assert!(display.contains("400"));
132 assert!(display.contains("invalid_grant"));
133 }
134
135 #[test]
136 fn auth_error_token_exchange_status() {
137 let err = AuthError::TokenExchange {
138 status: 401,
139 message: "unauthorized".to_string(),
140 };
141 match err {
142 AuthError::TokenExchange { status, message } => {
143 assert_eq!(status, 401);
144 assert_eq!(message, "unauthorized");
145 }
146 _ => panic!("Wrong error type"),
147 }
148 }
149
150 #[test]
151 fn spotify_auth_url_building() {
152 let url = SpotifyAuth::url("/api/token");
153 assert!(url.contains("/api/token"));
154 assert!(url.starts_with("https://"));
155 }
156
157 #[test]
158 fn spotify_auth_default() {
159 let _auth = SpotifyAuth::default();
160 }
162
163 #[test]
164 fn spotify_auth_new() {
165 let _auth = SpotifyAuth::new();
166 }
168
169 #[test]
170 fn auth_error_debug() {
171 let err = AuthError::TokenExchange {
172 status: 500,
173 message: "server error".to_string(),
174 };
175 let debug = format!("{:?}", err);
176 assert!(debug.contains("TokenExchange"));
177 assert!(debug.contains("500"));
178 }
179}