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