(** Configuration loading: round-tripping, backward compatibility, and rejection of malformed files and out-of-range values. *) open Test_helpers let test_round_trip () = with_temp_file "ogit" ".toml" (fun file -> let config = Ogit.Config. { user_name = "alice"; default_branch = "main"; git_project_root = "/srv/git"; commits_max_displayed = 25; root_title = ""; nav_logo = "/branding/logo.svg"; host = "127.0.0.1"; port = 9000; favorite_repositories = []; archived_repositories = []; } in Ogit.Config.write_file ~file (Ogit.Config.to_table config); match Ogit.Config.read_file ~file () with | Ok config' -> Alcotest.(check string) "user_name" config.user_name config'.user_name; Alcotest.(check string) "branch" config.default_branch config'.default_branch; Alcotest.(check string) "root" config.git_project_root config'.git_project_root; Alcotest.(check int) "commits" config.commits_max_displayed config'.commits_max_displayed; Alcotest.(check string) "host" config.host config'.host; Alcotest.(check string) "nav_logo" config.nav_logo config'.nav_logo; Alcotest.(check int) "port" config.port config'.port | Error error -> fail_config_error error) let test_backward_compat () = with_temp_file "ogit" ".toml" (fun file -> Out_channel.with_open_text file (fun channel -> Printf.fprintf channel "default_branch = \"main\"\n\ git_project_root = \"/srv/git\"\n\ commits_max_displayed = 10\n\ title = \"Legacy repositories\"\n"); match Ogit.Config.read_file ~file () with | Ok config -> Alcotest.(check string) "default user_name" "" config.user_name; Alcotest.(check string) "legacy title" "Legacy repositories" config.root_title; Alcotest.(check string) "default nav logo" "/static/git_icon.svg" config.nav_logo; Alcotest.(check string) "default host" "127.0.0.1" config.host; Alcotest.(check int) "default port" 8081 config.port | Error error -> fail_config_error error) (* Every key has a default, so a file overriding a single setting is valid; the untouched settings keep their defaults. *) let test_partial_file () = with_temp_file "ogit-partial" ".toml" (fun file -> Out_channel.with_open_text file (fun channel -> output_string channel "port = 9999\n"); match Ogit.Config.read_file ~file () with | Ok config -> Alcotest.(check int) "overridden port" 9999 config.port; Alcotest.(check string) "default root" Ogit.Config.default.git_project_root config.git_project_root; Alcotest.(check string) "default branch" Ogit.Config.default.default_branch config.default_branch; Alcotest.(check int) "default commits" Ogit.Config.default.commits_max_displayed config.commits_max_displayed | Error error -> fail_config_error error) (* A key that is present with the wrong type is still an error: only absence falls back to the default. *) let test_mistyped_key () = with_temp_file "ogit-mistyped" ".toml" (fun file -> Out_channel.with_open_text file (fun channel -> output_string channel "git_project_root = 5\n"); match Ogit.Config.read_file ~file () with | Error (Ogit.Config.Invalid_value _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "mistyped key was accepted") let test_malformed () = with_temp_file "ogit-malformed" ".toml" (fun file -> Out_channel.with_open_text file (fun channel -> output_string channel "user = [\n"); match Ogit.Config.read_file ~file () with | Error (Ogit.Config.Parse_error _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "malformed configuration was accepted") let test_invalid_value () = with_temp_file "ogit-invalid" ".toml" (fun file -> let invalid = Ogit.Config.{ default with commits_max_displayed = 0 } in Ogit.Config.write_file ~file (Ogit.Config.to_table invalid); match Ogit.Config.read_file ~file () with | Error (Ogit.Config.Invalid_value _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "invalid commit limit was accepted") let test_invalid_port_high () = with_temp_file "ogit-port-high" ".toml" (fun file -> let invalid = Ogit.Config.{ default with port = 65536 } in Ogit.Config.write_file ~file (Ogit.Config.to_table invalid); match Ogit.Config.read_file ~file () with | Error (Ogit.Config.Invalid_value _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "port 65536 was accepted") let test_invalid_port_zero () = with_temp_file "ogit-port-zero" ".toml" (fun file -> let invalid = Ogit.Config.{ default with port = 0 } in Ogit.Config.write_file ~file (Ogit.Config.to_table invalid); match Ogit.Config.read_file ~file () with | Error (Ogit.Config.Invalid_value _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "port 0 was accepted") let test_directory_rejected () = with_temp_directory "ogit-config-directory" (fun directory -> match Ogit.Config.read_file ~file:directory () with | Error (Ogit.Config.Io_error _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "a directory was accepted as a config file") let test_invalid_implicit () = with_temp_directory "ogit-xdg" (fun config_home -> let directory = Filename.concat config_home "ogit" in Unix.mkdir directory 0o755; let file = Filename.concat directory "config.toml" in Out_channel.with_open_text file (fun channel -> output_string channel "user = [\n"); with_environment "OGIT_CONFIG" "" (fun () -> with_environment "XDG_CONFIG_HOME" config_home (fun () -> match Ogit.Config.load () with | Error (Ogit.Config.Parse_error _) -> () | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "invalid implicit config should fail"))) let test_explicit_missing () = let missing = Filename.temp_file "ogit-missing" ".toml" in Sys.remove missing; with_environment "OGIT_CONFIG" missing (fun () -> match Ogit.Config.load () with | Error (Ogit.Config.Not_found file) -> Alcotest.(check string) "path" missing file | Error error -> fail_config_error error | Ok _ -> Alcotest.fail "explicitly missing config should fail") let test_location () = with_environment "OGIT_CONFIG" "" (fun () -> with_environment "XDG_CONFIG_HOME" "/tmp/xdg-config" (fun () -> let expected = Filename.concat (Filename.concat "/tmp/xdg-config" "ogit") "config.toml" in Alcotest.(check string) "XDG path" expected (Ogit.Config.locate_config_file ()))); with_environment "OGIT_CONFIG" "/tmp/custom-ogit.toml" (fun () -> Alcotest.(check string) "explicit path" "/tmp/custom-ogit.toml" (Ogit.Config.locate_config_file ())) let suite = ( "config", [ Alcotest.test_case "round trip" `Quick test_round_trip; Alcotest.test_case "backward compat" `Quick test_backward_compat; Alcotest.test_case "partial file" `Quick test_partial_file; Alcotest.test_case "mistyped key" `Quick test_mistyped_key; Alcotest.test_case "malformed" `Quick test_malformed; Alcotest.test_case "invalid value" `Quick test_invalid_value; Alcotest.test_case "port too high" `Quick test_invalid_port_high; Alcotest.test_case "port zero" `Quick test_invalid_port_zero; Alcotest.test_case "directory rejected" `Quick test_directory_rejected; Alcotest.test_case "invalid implicit" `Quick test_invalid_implicit; Alcotest.test_case "explicit missing" `Quick test_explicit_missing; Alcotest.test_case "location" `Quick test_location; ] )