1use crate::{HarmontClient, Result};
9
10impl HarmontClient {
11 pub async fn redeem_code(&self, code: &str) -> Result<String> {
17 let url = format!("{}/api/v0/auth/cli/redeem", self.base);
18 let resp = self.http.post(&url)
19 .json(&serde_json::json!({ "code": code }))
20 .send().await?;
21 #[derive(serde::Deserialize)]
22 struct R { token: String }
23 let r: R = self.parse_json(resp).await?;
24 Ok(r.token)
25 }
26
27 pub async fn claim_token(&self, nonce: &str) -> Result<String> {
35 let url = format!("{}/api/v0/auth/cli/claim", self.base);
36 let resp = self.http.post(&url)
37 .json(&serde_json::json!({ "nonce": nonce }))
38 .send().await?;
39 #[derive(serde::Deserialize)]
40 struct R { token: String }
41 let r: R = self.parse_json(resp).await?;
42 Ok(r.token)
43 }
44
45 pub async fn create_api_token(&self, description: &str) -> Result<String> {
50 let url = format!("{}/api/v0/user/api-tokens", self.base);
51 let resp = self.http.post(&url)
52 .json(&serde_json::json!({ "description": description }))
53 .send().await?;
54 #[derive(serde::Deserialize)]
55 struct R { token: String }
56 let r: R = self.parse_json(resp).await?;
57 Ok(r.token)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use crate::HarmontClient;
64 use wiremock::matchers::{body_partial_json, method, path};
65 use wiremock::{Mock, MockServer, ResponseTemplate};
66 use serde_json::json;
67
68 #[tokio::test]
69 async fn redeem_returns_token() {
70 let server = MockServer::start().await;
71 Mock::given(method("POST"))
72 .and(path("/api/v0/auth/cli/redeem"))
73 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"token": "hm_live"})))
74 .mount(&server).await;
75 let client = HarmontClient::anonymous(server.uri());
76 let tok = client.redeem_code("ABCD-1234").await.expect("ok");
77 assert_eq!(tok, "hm_live");
78 }
79
80 #[tokio::test]
81 async fn redeem_sends_code_field() {
82 let server = MockServer::start().await;
83 Mock::given(method("POST"))
84 .and(path("/api/v0/auth/cli/redeem"))
85 .and(body_partial_json(json!({"code": "ZZZZ-9999"})))
86 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"token": "hm_abc"})))
87 .mount(&server).await;
88 let client = HarmontClient::anonymous(server.uri());
89 client.redeem_code("ZZZZ-9999").await.expect("ok");
90 }
91
92 #[tokio::test]
93 async fn redeem_invalid_code_maps_to_api_error() {
94 let server = MockServer::start().await;
95 Mock::given(method("POST"))
96 .and(path("/api/v0/auth/cli/redeem"))
97 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
98 "error": {"code": "cli_code_invalid", "message": "This CLI code is invalid, expired, or already used."}
99 })))
100 .mount(&server).await;
101 let client = HarmontClient::anonymous(server.uri());
102 let err = client.redeem_code("BAD-CODE").await.unwrap_err();
103 match err {
104 crate::HarmontError::Api { status, code, .. } => {
105 assert_eq!(status, 400);
106 assert_eq!(code, "cli_code_invalid");
107 }
108 other => panic!("expected Api error, got {other:?}"),
109 }
110 }
111
112 #[tokio::test]
113 async fn claim_returns_token() {
114 let server = MockServer::start().await;
115 Mock::given(method("POST"))
116 .and(path("/api/v0/auth/cli/claim"))
117 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"token": "hm_session"})))
118 .mount(&server).await;
119 let client = HarmontClient::anonymous(server.uri());
120 let tok = client.claim_token("my-random-nonce-xyz").await.expect("ok");
121 assert_eq!(tok, "hm_session");
122 }
123
124 #[tokio::test]
125 async fn claim_sends_nonce_field() {
126 let server = MockServer::start().await;
127 Mock::given(method("POST"))
128 .and(path("/api/v0/auth/cli/claim"))
129 .and(body_partial_json(json!({"nonce": "abc-nonce-123"})))
130 .respond_with(ResponseTemplate::new(200).set_body_json(json!({"token": "hm_tok"})))
131 .mount(&server).await;
132 let client = HarmontClient::anonymous(server.uri());
133 client.claim_token("abc-nonce-123").await.expect("ok");
134 }
135
136 #[tokio::test]
137 async fn claim_not_yet_available_maps_to_api_error() {
138 let server = MockServer::start().await;
139 Mock::given(method("POST"))
140 .and(path("/api/v0/auth/cli/claim"))
141 .respond_with(ResponseTemplate::new(400).set_body_json(json!({
142 "error": {"code": "cli_code_invalid", "message": "No token parked for this nonce yet."}
143 })))
144 .mount(&server).await;
145 let client = HarmontClient::anonymous(server.uri());
146 let err = client.claim_token("no-match-nonce").await.unwrap_err();
147 match err {
148 crate::HarmontError::Api { status, code, .. } => {
149 assert_eq!(status, 400);
150 assert_eq!(code, "cli_code_invalid");
151 }
152 other => panic!("expected Api error, got {other:?}"),
153 }
154 }
155
156 #[tokio::test]
157 async fn create_api_token_returns_raw_token() {
158 let server = MockServer::start().await;
159 Mock::given(method("POST"))
160 .and(path("/api/v0/user/api-tokens"))
161 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
162 "token": "hm_personal_abc123",
163 "api_token": {
164 "id": "00000000-0000-0000-0000-000000000001",
165 "description": "my local machine",
166 "created_at": "2026-06-04T00:00:00Z",
167 "expires_at": null,
168 "last_used_at": null
169 }
170 })))
171 .mount(&server).await;
172 let client = HarmontClient::with_base_url("hm_session_token", server.uri());
173 let raw = client.create_api_token("my local machine").await.expect("ok");
174 assert_eq!(raw, "hm_personal_abc123");
175 }
176
177 #[tokio::test]
178 async fn create_api_token_sends_description_field() {
179 let server = MockServer::start().await;
180 Mock::given(method("POST"))
181 .and(path("/api/v0/user/api-tokens"))
182 .and(body_partial_json(json!({"description": "laptop key"})))
183 .respond_with(ResponseTemplate::new(201).set_body_json(json!({
184 "token": "hm_xyz",
185 "api_token": {
186 "id": "00000000-0000-0000-0000-000000000002",
187 "description": "laptop key",
188 "created_at": "2026-06-04T00:00:00Z",
189 "expires_at": null,
190 "last_used_at": null
191 }
192 })))
193 .mount(&server).await;
194 let client = HarmontClient::with_base_url("hm_session", server.uri());
195 client.create_api_token("laptop key").await.expect("ok");
196 }
197
198 #[tokio::test]
199 async fn create_api_token_unauthorized_maps_cleanly() {
200 let server = MockServer::start().await;
201 Mock::given(method("POST"))
202 .and(path("/api/v0/user/api-tokens"))
203 .respond_with(ResponseTemplate::new(401))
204 .mount(&server).await;
205 let client = HarmontClient::with_base_url("bad_token", server.uri());
206 let err = client.create_api_token("test").await.unwrap_err();
207 assert!(matches!(err, crate::HarmontError::Unauthorized));
208 }
209}