1use credentials_provider::CredentialsProvider;
2use gpui::App;
3use util::ResultExt as _;
4
5const GOOGLE_AI_EXTENSION_ID: &str = "google-ai";
6const GOOGLE_AI_PROVIDER_ID: &str = "google-ai";
7const GOOGLE_AI_DEFAULT_API_URL: &str = "https://generativelanguage.googleapis.com";
8
9pub fn migrate_google_ai_credentials_if_needed(extension_id: &str, cx: &mut App) {
10 if extension_id != GOOGLE_AI_EXTENSION_ID {
11 return;
12 }
13
14 let extension_credential_key = format!(
15 "extension-llm-{}:{}",
16 GOOGLE_AI_EXTENSION_ID, GOOGLE_AI_PROVIDER_ID
17 );
18
19 let credentials_provider = <dyn CredentialsProvider>::global(cx);
20
21 cx.spawn(async move |cx| {
22 let existing_credential = credentials_provider
23 .read_credentials(&extension_credential_key, &cx)
24 .await
25 .ok()
26 .flatten();
27
28 if existing_credential.is_some() {
29 log::debug!("Google AI extension already has credentials, skipping migration");
30 return;
31 }
32
33 let old_credential = credentials_provider
34 .read_credentials(GOOGLE_AI_DEFAULT_API_URL, &cx)
35 .await
36 .ok()
37 .flatten();
38
39 let api_key = match old_credential {
40 Some((_, key_bytes)) => match String::from_utf8(key_bytes) {
41 Ok(key) if !key.is_empty() => key,
42 Ok(_) => {
43 log::debug!("Existing Google AI API key is empty, marking as migrated");
44 String::new()
45 }
46 Err(_) => {
47 log::error!("Failed to decode Google AI API key as UTF-8");
48 return;
49 }
50 },
51 None => {
52 log::debug!("No existing Google AI API key found, marking as migrated");
53 String::new()
54 }
55 };
56
57 if api_key.is_empty() {
58 // Write empty credentials as a marker that migration was attempted
59 credentials_provider
60 .write_credentials(&extension_credential_key, "Bearer", b"", &cx)
61 .await
62 .log_err();
63 return;
64 }
65
66 log::info!("Migrating existing Google AI API key to Google AI extension");
67
68 match credentials_provider
69 .write_credentials(&extension_credential_key, "Bearer", api_key.as_bytes(), &cx)
70 .await
71 {
72 Ok(()) => {
73 log::info!("Successfully migrated Google AI API key to extension");
74 }
75 Err(err) => {
76 log::error!("Failed to migrate Google AI API key: {}", err);
77 }
78 }
79 })
80 .detach();
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use gpui::TestAppContext;
87
88 #[gpui::test]
89 async fn test_migrates_credentials_from_old_location(cx: &mut TestAppContext) {
90 let api_key = "AIzaSy-test-key-12345";
91
92 cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", api_key.as_bytes());
93
94 cx.update(|cx| {
95 migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
96 });
97
98 cx.run_until_parked();
99
100 let migrated = cx.read_credentials("extension-llm-google-ai:google-ai");
101 assert!(migrated.is_some(), "Credentials should have been migrated");
102 let (username, password) = migrated.unwrap();
103 assert_eq!(username, "Bearer");
104 assert_eq!(String::from_utf8(password).unwrap(), api_key);
105 }
106
107 #[gpui::test]
108 async fn test_skips_migration_if_extension_already_has_credentials(cx: &mut TestAppContext) {
109 let old_api_key = "AIzaSy-old-key";
110 let existing_key = "AIzaSy-existing-key";
111
112 cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
113 cx.write_credentials(
114 "extension-llm-google-ai:google-ai",
115 "Bearer",
116 existing_key.as_bytes(),
117 );
118
119 cx.update(|cx| {
120 migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
121 });
122
123 cx.run_until_parked();
124
125 let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
126 let (_, password) = credentials.unwrap();
127 assert_eq!(
128 String::from_utf8(password).unwrap(),
129 existing_key,
130 "Should not overwrite existing credentials"
131 );
132 }
133
134 #[gpui::test]
135 async fn test_skips_migration_if_empty_marker_exists(cx: &mut TestAppContext) {
136 let old_api_key = "AIzaSy-old-key";
137
138 // Old credentials exist
139 cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", old_api_key.as_bytes());
140 // But empty marker already exists (from previous migration attempt)
141 cx.write_credentials("extension-llm-google-ai:google-ai", "Bearer", b"");
142
143 cx.update(|cx| {
144 migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
145 });
146
147 cx.run_until_parked();
148
149 let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
150 let (_, password) = credentials.unwrap();
151 assert!(
152 password.is_empty(),
153 "Should not overwrite empty marker with old credentials"
154 );
155 }
156
157 #[gpui::test]
158 async fn test_writes_empty_marker_if_no_old_credentials(cx: &mut TestAppContext) {
159 cx.update(|cx| {
160 migrate_google_ai_credentials_if_needed(GOOGLE_AI_EXTENSION_ID, cx);
161 });
162
163 cx.run_until_parked();
164
165 let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
166 assert!(
167 credentials.is_some(),
168 "Should write empty credentials as migration marker"
169 );
170 let (username, password) = credentials.unwrap();
171 assert_eq!(username, "Bearer");
172 assert!(password.is_empty(), "Password should be empty marker");
173 }
174
175 #[gpui::test]
176 async fn test_skips_migration_for_other_extensions(cx: &mut TestAppContext) {
177 let api_key = "AIzaSy-test-key";
178
179 cx.write_credentials(GOOGLE_AI_DEFAULT_API_URL, "Bearer", api_key.as_bytes());
180
181 cx.update(|cx| {
182 migrate_google_ai_credentials_if_needed("some-other-extension", cx);
183 });
184
185 cx.run_until_parked();
186
187 let credentials = cx.read_credentials("extension-llm-google-ai:google-ai");
188 assert!(
189 credentials.is_none(),
190 "Should not migrate for other extensions"
191 );
192 }
193}