Skip to main content

gen_models/
errors.rs

1use gen_core::errors::{ConfigError, ConnectionError, StrandError};
2use thiserror::Error;
3
4#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
5pub enum QueryError {
6    #[error("Results not found: {0}")]
7    ResultsNotFound(String),
8}
9
10#[derive(Debug, Error, PartialEq)]
11pub enum ChangeError {
12    #[error("Operation Error: {0}")]
13    OutOfBounds(String),
14}
15
16#[derive(Debug, Error, PartialEq)]
17pub enum ChangesetError {
18    #[error("Strand Error: {0}")]
19    StrandError(#[from] StrandError),
20    #[error("Missing Model: {0}")]
21    MissingModel(String),
22    #[error("SQLite Error: {0}")]
23    SqliteError(#[from] rusqlite::Error),
24}
25
26#[derive(Debug, PartialEq, Error)]
27pub enum OperationError {
28    #[error("Changeset Error: {0}")]
29    ChangesetError(#[from] ChangesetError),
30    #[error("Changeset Error: {0}")]
31    ConnectionError(#[from] ConnectionError),
32    #[error("No Changes")]
33    NoChanges,
34    #[error("Operation Already Exists")]
35    OperationExists,
36    #[error("Operation {0} Does Not Exist")]
37    NoOperation(String),
38    #[error("SQL Error: {0}")]
39    SQLError(String),
40    #[error("SQLite Error: {0}")]
41    SqliteError(#[from] rusqlite::Error),
42    #[error("Config Error: {0}")]
43    ConfigError(#[from] ConfigError),
44    #[error("Error storing data file")]
45    IOError,
46}
47
48#[derive(Debug, PartialEq, Error)]
49pub enum BranchError {
50    #[error("Cannot delete branch: {0}")]
51    CannotDelete(String),
52    #[error("SQL Error: {0}")]
53    SQLError(String),
54    #[error("SQLite Error: {0}")]
55    SqliteError(#[from] rusqlite::Error),
56}
57
58#[derive(Debug, Error, PartialEq)]
59pub enum RemoteError {
60    #[error("Remote '{0}' already exists")]
61    RemoteAlreadyExists(String),
62
63    #[error("Remote '{0}' not found")]
64    RemoteNotFound(String),
65
66    #[error("Invalid remote URL: {0}")]
67    InvalidUrl(String),
68
69    #[error("Invalid remote name: {0}")]
70    InvalidName(String),
71
72    #[error("Remote name cannot be empty")]
73    EmptyName,
74
75    #[error("Remote URL cannot be empty")]
76    EmptyUrl,
77
78    #[error("Remote name can only contain letters, numbers, hyphens, and underscores")]
79    InvalidNameCharacters,
80
81    #[error("URL must use HTTP, HTTPS, SSH protocol, or be a valid file path")]
82    UnsupportedUrlScheme,
83
84    #[error("Database error: {0}")]
85    DatabaseError(#[from] rusqlite::Error),
86
87    #[error("Cannot delete remote '{0}' as it is set as the default remote")]
88    CannotDeleteDefaultRemote(String),
89
90    #[error("Cannot set non-existent remote '{0}' as default")]
91    DefaultRemoteNotFound(String),
92}
93
94#[derive(Debug, Error)]
95pub enum FileAdditionError {
96    #[error("Failed to read file: {0}")]
97    FileReadError(#[from] std::io::Error),
98
99    #[error("File not found: {0}")]
100    FileNotFound(String),
101
102    #[error("Permission denied accessing file: {0}")]
103    FilePermissionDenied(String),
104
105    #[error("Failed to calculate checksum: {0}")]
106    ChecksumError(String),
107
108    #[error("Database error: {0}")]
109    DatabaseError(#[from] rusqlite::Error),
110
111    #[error("HashId generation failed: {0}")]
112    HashIdError(String),
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    mod remote_error_tests {
120        use super::*;
121        use crate::{
122            db::OperationsConnection,
123            operations::{Branch, Defaults, Remote},
124            test_helpers::get_operation_connection,
125        };
126
127        fn setup_test_db() -> OperationsConnection {
128            get_operation_connection(None).unwrap()
129        }
130
131        #[test]
132        fn test_remote_validation_empty_name() {
133            let result = Remote::validate_name("");
134            assert_eq!(result, Err(RemoteError::EmptyName));
135        }
136
137        #[test]
138        fn test_remote_validation_invalid_name_characters() {
139            // Test various invalid characters
140            assert_eq!(
141                Remote::validate_name("remote with spaces"),
142                Err(RemoteError::InvalidNameCharacters)
143            );
144            assert_eq!(
145                Remote::validate_name("remote@special"),
146                Err(RemoteError::InvalidNameCharacters)
147            );
148            assert_eq!(
149                Remote::validate_name("remote.dot"),
150                Err(RemoteError::InvalidNameCharacters)
151            );
152            assert_eq!(
153                Remote::validate_name("remote/slash"),
154                Err(RemoteError::InvalidNameCharacters)
155            );
156            assert_eq!(
157                Remote::validate_name("remote\\backslash"),
158                Err(RemoteError::InvalidNameCharacters)
159            );
160            assert_eq!(
161                Remote::validate_name("remote:colon"),
162                Err(RemoteError::InvalidNameCharacters)
163            );
164            assert_eq!(
165                Remote::validate_name("remote;semicolon"),
166                Err(RemoteError::InvalidNameCharacters)
167            );
168            assert_eq!(
169                Remote::validate_name("remote|pipe"),
170                Err(RemoteError::InvalidNameCharacters)
171            );
172            assert_eq!(
173                Remote::validate_name("remote<bracket"),
174                Err(RemoteError::InvalidNameCharacters)
175            );
176            assert_eq!(
177                Remote::validate_name("remote>bracket"),
178                Err(RemoteError::InvalidNameCharacters)
179            );
180            assert_eq!(
181                Remote::validate_name("remote?question"),
182                Err(RemoteError::InvalidNameCharacters)
183            );
184            assert_eq!(
185                Remote::validate_name("remote*asterisk"),
186                Err(RemoteError::InvalidNameCharacters)
187            );
188            assert_eq!(
189                Remote::validate_name("remote+plus"),
190                Err(RemoteError::InvalidNameCharacters)
191            );
192            assert_eq!(
193                Remote::validate_name("remote=equals"),
194                Err(RemoteError::InvalidNameCharacters)
195            );
196            assert_eq!(
197                Remote::validate_name("remote[bracket"),
198                Err(RemoteError::InvalidNameCharacters)
199            );
200            assert_eq!(
201                Remote::validate_name("remote]bracket"),
202                Err(RemoteError::InvalidNameCharacters)
203            );
204            assert_eq!(
205                Remote::validate_name("remote{brace"),
206                Err(RemoteError::InvalidNameCharacters)
207            );
208            assert_eq!(
209                Remote::validate_name("remote}brace"),
210                Err(RemoteError::InvalidNameCharacters)
211            );
212            assert_eq!(
213                Remote::validate_name("remote(paren"),
214                Err(RemoteError::InvalidNameCharacters)
215            );
216            assert_eq!(
217                Remote::validate_name("remote)paren"),
218                Err(RemoteError::InvalidNameCharacters)
219            );
220            assert_eq!(
221                Remote::validate_name("remote\"quote"),
222                Err(RemoteError::InvalidNameCharacters)
223            );
224            assert_eq!(
225                Remote::validate_name("remote'quote"),
226                Err(RemoteError::InvalidNameCharacters)
227            );
228            assert_eq!(
229                Remote::validate_name("remote`backtick"),
230                Err(RemoteError::InvalidNameCharacters)
231            );
232            assert_eq!(
233                Remote::validate_name("remote~tilde"),
234                Err(RemoteError::InvalidNameCharacters)
235            );
236            assert_eq!(
237                Remote::validate_name("remote!exclamation"),
238                Err(RemoteError::InvalidNameCharacters)
239            );
240            assert_eq!(
241                Remote::validate_name("remote#hash"),
242                Err(RemoteError::InvalidNameCharacters)
243            );
244            assert_eq!(
245                Remote::validate_name("remote$dollar"),
246                Err(RemoteError::InvalidNameCharacters)
247            );
248            assert_eq!(
249                Remote::validate_name("remote%percent"),
250                Err(RemoteError::InvalidNameCharacters)
251            );
252            assert_eq!(
253                Remote::validate_name("remote^caret"),
254                Err(RemoteError::InvalidNameCharacters)
255            );
256            assert_eq!(
257                Remote::validate_name("remote&ampersand"),
258                Err(RemoteError::InvalidNameCharacters)
259            );
260        }
261
262        #[test]
263        fn test_remote_validation_valid_names() {
264            // Test valid names
265            assert!(Remote::validate_name("origin").is_ok());
266            assert!(Remote::validate_name("my-remote").is_ok());
267            assert!(Remote::validate_name("remote_1").is_ok());
268            assert!(Remote::validate_name("test123").is_ok());
269            assert!(Remote::validate_name("UPPERCASE").is_ok());
270            assert!(Remote::validate_name("MixedCase").is_ok());
271            assert!(Remote::validate_name("remote-with-many-hyphens").is_ok());
272            assert!(Remote::validate_name("remote_with_many_underscores").is_ok());
273            assert!(Remote::validate_name("remote123with456numbers").is_ok());
274            assert!(Remote::validate_name("a").is_ok()); // Single character
275            assert!(Remote::validate_name("a1").is_ok());
276            assert!(Remote::validate_name("1a").is_ok());
277            assert!(Remote::validate_name("123").is_ok()); // All numbers
278            assert!(Remote::validate_name("remote-123_test").is_ok()); // Mixed valid characters
279        }
280
281        #[test]
282        fn test_url_validation_empty_url() {
283            let result = Remote::validate_url("");
284            assert_eq!(result, Err(RemoteError::EmptyUrl));
285        }
286
287        #[test]
288        fn test_url_validation_valid_urls() {
289            // Valid HTTP/HTTPS URLs
290            assert!(Remote::validate_url("https://genhub.bio/user/repo.gen").is_ok());
291            assert!(Remote::validate_url("http://example.com/repo").is_ok());
292            assert!(Remote::validate_url("https://gitlab.com/user/repo").is_ok());
293            assert!(Remote::validate_url("http://localhost:8080/repo").is_ok());
294
295            // Valid SSH URLs
296            assert!(Remote::validate_url("ssh://git@genhub.bio/user/repo.gen").is_ok());
297            assert!(Remote::validate_url("ssh://user@host.com:2222/path/to/repo").is_ok());
298
299            // Valid directories
300            assert!(Remote::validate_url("/path/to/local/repo").is_ok());
301            assert!(Remote::validate_url("/home/user/repos/myrepo").is_ok());
302            assert!(Remote::validate_url("/tmp/test").is_ok());
303            assert!(Remote::validate_url("file:///tmp/test").is_ok());
304
305            // Valid SSH-style paths
306            assert!(Remote::validate_url("user@host:path/to/repo").is_ok());
307            assert!(Remote::validate_url("git@genhub.bio:user/repo.gen").is_ok());
308            assert!(Remote::validate_url("user@server.com:~/repos/project").is_ok());
309        }
310
311        #[test]
312        fn test_url_validation_invalid_urls() {
313            // Invalid scheme
314            assert_eq!(
315                Remote::validate_url("ftp://invalid-protocol.com"),
316                Err(RemoteError::UnsupportedUrlScheme)
317            );
318            assert_eq!(
319                Remote::validate_url("smb://network/share"),
320                Err(RemoteError::UnsupportedUrlScheme)
321            );
322
323            // Invalid format
324            assert_eq!(
325                Remote::validate_url("not-a-url"),
326                Err(RemoteError::UnsupportedUrlScheme)
327            );
328            assert_eq!(
329                Remote::validate_url("just-text"),
330                Err(RemoteError::UnsupportedUrlScheme)
331            );
332            assert_eq!(
333                Remote::validate_url("no-scheme-or-path"),
334                Err(RemoteError::UnsupportedUrlScheme)
335            );
336
337            // Malformed URLs with schemes
338            assert!(matches!(
339                Remote::validate_url("https://"),
340                Err(RemoteError::InvalidUrl(_))
341            ));
342            assert!(matches!(
343                Remote::validate_url("http://[invalid"),
344                Err(RemoteError::InvalidUrl(_))
345            ));
346        }
347
348        #[test]
349        fn test_remote_create_success() {
350            let conn = setup_test_db();
351
352            let result = Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen");
353            assert!(result.is_ok());
354
355            let remote = result.unwrap();
356            assert_eq!(remote.name, "origin");
357            assert_eq!(remote.url, "https://genhub.bio/user/repo.gen");
358        }
359
360        #[test]
361        fn test_remote_create_duplicate_name() {
362            let conn = setup_test_db();
363
364            // Create first remote
365            Remote::create(&conn, "origin", "https://genhub.bio/user/repo1.gen").unwrap();
366
367            // Try to create duplicate
368            let result = Remote::create(&conn, "origin", "https://genhub.bio/user/repo2.gen");
369            assert_eq!(
370                result,
371                Err(RemoteError::RemoteAlreadyExists("origin".to_string()))
372            );
373        }
374
375        #[test]
376        fn test_remote_create_invalid_name() {
377            let conn = setup_test_db();
378
379            let result = Remote::create(&conn, "", "https://genhub.bio/user/repo.gen");
380            assert_eq!(result, Err(RemoteError::EmptyName));
381
382            let result = Remote::create(&conn, "invalid name", "https://genhub.bio/user/repo.gen");
383            assert_eq!(result, Err(RemoteError::InvalidNameCharacters));
384        }
385
386        #[test]
387        fn test_remote_create_invalid_url() {
388            let conn = setup_test_db();
389
390            let result = Remote::create(&conn, "origin", "");
391            assert_eq!(result, Err(RemoteError::EmptyUrl));
392
393            let result = Remote::create(&conn, "origin", "ftp://invalid.com");
394            assert_eq!(result, Err(RemoteError::UnsupportedUrlScheme));
395        }
396
397        #[test]
398        fn test_remote_get_by_name_success() {
399            let conn = setup_test_db();
400
401            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
402
403            let result = Remote::get_by_name(&conn, "origin");
404            assert!(result.is_ok());
405
406            let remote = result.unwrap();
407            assert_eq!(remote.name, "origin");
408            assert_eq!(remote.url, "https://genhub.bio/user/repo.gen");
409        }
410
411        #[test]
412        fn test_remote_get_by_name_not_found() {
413            let conn = setup_test_db();
414
415            let result = Remote::get_by_name(&conn, "nonexistent");
416            assert_eq!(
417                result,
418                Err(RemoteError::RemoteNotFound("nonexistent".to_string()))
419            );
420        }
421
422        #[test]
423        fn test_remote_delete_success() {
424            let conn = setup_test_db();
425
426            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
427
428            let result = Remote::delete(&conn, "origin");
429            assert!(result.is_ok());
430
431            // Verify it's deleted
432            let get_result = Remote::get_by_name(&conn, "origin");
433            assert_eq!(
434                get_result,
435                Err(RemoteError::RemoteNotFound("origin".to_string()))
436            );
437        }
438
439        #[test]
440        fn test_remote_delete_not_found() {
441            let conn = setup_test_db();
442
443            let result = Remote::delete(&conn, "nonexistent");
444            assert_eq!(
445                result,
446                Err(RemoteError::RemoteNotFound("nonexistent".to_string()))
447            );
448        }
449
450        #[test]
451        fn test_remote_exists() {
452            let conn = setup_test_db();
453
454            assert!(!Remote::exists(&conn, "origin"));
455
456            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
457            assert!(Remote::exists(&conn, "origin"));
458
459            Remote::delete(&conn, "origin").unwrap();
460            assert!(!Remote::exists(&conn, "origin"));
461        }
462
463        #[test]
464        fn test_defaults_set_default_remote_success() {
465            let conn = setup_test_db();
466
467            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
468
469            let result = Defaults::set_default_remote(&conn, Some("origin"));
470            assert!(result.is_ok());
471
472            let default = Defaults::get_default_remote(&conn);
473            assert_eq!(default, Some("origin".to_string()));
474        }
475
476        #[test]
477        fn test_defaults_set_default_remote_not_found() {
478            let conn = setup_test_db();
479
480            let result = Defaults::set_default_remote(&conn, Some("nonexistent"));
481            assert_eq!(
482                result,
483                Err(RemoteError::RemoteNotFound("nonexistent".to_string()))
484            );
485        }
486
487        #[test]
488        fn test_defaults_set_default_remote_clear() {
489            let conn = setup_test_db();
490
491            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
492            Defaults::set_default_remote(&conn, Some("origin")).unwrap();
493
494            let result = Defaults::set_default_remote(&conn, None);
495            assert!(result.is_ok());
496
497            let default = Defaults::get_default_remote(&conn);
498            assert_eq!(default, None);
499        }
500
501        #[test]
502        fn test_defaults_get_default_remote_url() {
503            let conn = setup_test_db();
504
505            // No default set
506            assert_eq!(Defaults::get_default_remote_url(&conn), None);
507
508            // Set default
509            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
510            Defaults::set_default_remote(&conn, Some("origin")).unwrap();
511
512            let url = Defaults::get_default_remote_url(&conn);
513            assert_eq!(url, Some("https://genhub.bio/user/repo.gen".to_string()));
514
515            // Delete remote (should return None even though default is set)
516            Remote::delete(&conn, "origin").unwrap();
517            let url = Defaults::get_default_remote_url(&conn);
518            assert_eq!(url, None);
519        }
520
521        #[test]
522        fn test_branch_set_remote_validated_success() {
523            let conn = setup_test_db();
524
525            // Create a remote and branch
526            Remote::create(&conn, "origin", "https://genhub.bio/user/repo.gen").unwrap();
527
528            // For this test, we'll assume branch ID 1 exists (this would be set up by the test framework)
529            // In a real scenario, you'd create a branch first
530            let branch_id = 1i64;
531
532            let result = Branch::set_remote_validated(&conn, branch_id, Some("origin"));
533            assert!(result.is_ok());
534        }
535
536        #[test]
537        fn test_branch_set_remote_validated_remote_not_found() {
538            let conn = setup_test_db();
539
540            let branch_id = 1i64;
541
542            let result = Branch::set_remote_validated(&conn, branch_id, Some("nonexistent"));
543            assert_eq!(
544                result,
545                Err(RemoteError::RemoteNotFound("nonexistent".to_string()))
546            );
547        }
548
549        #[test]
550        fn test_branch_set_remote_validated_clear() {
551            let conn = setup_test_db();
552
553            let branch_id = 1i64;
554
555            let result = Branch::set_remote_validated(&conn, branch_id, None);
556            assert!(result.is_ok());
557        }
558
559        #[test]
560        fn test_error_display_messages() {
561            // Test that error messages are user-friendly
562            assert_eq!(
563                RemoteError::EmptyName.to_string(),
564                "Remote name cannot be empty"
565            );
566            assert_eq!(
567                RemoteError::EmptyUrl.to_string(),
568                "Remote URL cannot be empty"
569            );
570            assert_eq!(
571                RemoteError::InvalidNameCharacters.to_string(),
572                "Remote name can only contain letters, numbers, hyphens, and underscores"
573            );
574            assert_eq!(
575                RemoteError::UnsupportedUrlScheme.to_string(),
576                "URL must use HTTP, HTTPS, SSH protocol, or be a valid file path"
577            );
578            assert_eq!(
579                RemoteError::RemoteAlreadyExists("origin".to_string()).to_string(),
580                "Remote 'origin' already exists"
581            );
582            assert_eq!(
583                RemoteError::RemoteNotFound("origin".to_string()).to_string(),
584                "Remote 'origin' not found"
585            );
586            assert_eq!(
587                RemoteError::InvalidUrl("test".to_string()).to_string(),
588                "Invalid remote URL: test"
589            );
590            assert_eq!(
591                RemoteError::InvalidName("test".to_string()).to_string(),
592                "Invalid remote name: test"
593            );
594            assert_eq!(
595                RemoteError::CannotDeleteDefaultRemote("origin".to_string()).to_string(),
596                "Cannot delete remote 'origin' as it is set as the default remote"
597            );
598            assert_eq!(
599                RemoteError::DefaultRemoteNotFound("origin".to_string()).to_string(),
600                "Cannot set non-existent remote 'origin' as default"
601            );
602        }
603
604        #[test]
605        fn test_error_equality() {
606            // Test that errors can be compared for equality
607            assert_eq!(RemoteError::EmptyName, RemoteError::EmptyName);
608            assert_eq!(RemoteError::EmptyUrl, RemoteError::EmptyUrl);
609            assert_eq!(
610                RemoteError::InvalidNameCharacters,
611                RemoteError::InvalidNameCharacters
612            );
613            assert_eq!(
614                RemoteError::UnsupportedUrlScheme,
615                RemoteError::UnsupportedUrlScheme
616            );
617            assert_eq!(
618                RemoteError::RemoteAlreadyExists("test".to_string()),
619                RemoteError::RemoteAlreadyExists("test".to_string())
620            );
621            assert_eq!(
622                RemoteError::RemoteNotFound("test".to_string()),
623                RemoteError::RemoteNotFound("test".to_string())
624            );
625
626            // Test inequality
627            assert_ne!(RemoteError::EmptyName, RemoteError::EmptyUrl);
628            assert_ne!(
629                RemoteError::RemoteAlreadyExists("test1".to_string()),
630                RemoteError::RemoteAlreadyExists("test2".to_string())
631            );
632        }
633
634        #[test]
635        fn test_comprehensive_validation_edge_cases() {
636            // Test edge cases for name validation
637            assert!(Remote::validate_name("a").is_ok()); // Single character
638            assert!(Remote::validate_name("1").is_ok()); // Single digit
639            assert!(Remote::validate_name("-").is_ok()); // Single hyphen
640            assert!(Remote::validate_name("_").is_ok()); // Single underscore
641
642            // Test very long names (should be valid as long as they contain valid characters)
643            let long_name = "a".repeat(1000);
644            assert!(Remote::validate_name(&long_name).is_ok());
645
646            let long_name_with_valid_chars = "a-b_c".repeat(200);
647            assert!(Remote::validate_name(&long_name_with_valid_chars).is_ok());
648
649            // Test edge cases for URL validation
650            assert!(Remote::validate_url("https://a.b").is_ok()); // Minimal valid URL
651            assert!(Remote::validate_url("/a").is_ok()); // Minimal file path
652            assert!(Remote::validate_url("a:b").is_ok()); // Minimal SSH-style path
653        }
654    }
655}