Coverage for cli / tests / test_cli_options.py: 100%

69 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-06-19 09:10 +0000

1""" 

2Tests for the Typer `upload` command - argument validation, env-var wiring, and defaults. 

3""" 

4 

5from pathlib import Path 

6from unittest.mock import AsyncMock 

7 

8from typer.testing import CliRunner 

9 

10from covered.cli import app 

11 

12from utils import COMMON_ENV 

13 

14runner = CliRunner() 

15 

16 

17def test_upload_rejects_api_url_with_trailing_slash( 

18 mock_main: AsyncMock, tmp_path: Path 

19): 

20 """ 

21 `--api-url https://x/` (with trailing slash) exits non-zero with a clear message. 

22 """ 

23 env = {**COMMON_ENV, "COVERED_API_URL": "https://api.example.com/"} 

24 

25 result = runner.invoke(app, [str(tmp_path)], env=env) 

26 

27 assert result.exit_code != 0 

28 assert "must not end with a slash" in result.stderr 

29 assert "--api-url" in result.stderr 

30 mock_main.assert_not_awaited() 

31 

32 

33def test_upload_requires_existing_directory(mock_main: AsyncMock, tmp_path: Path): 

34 """ 

35 A directory argument that does not exist fails Typer's `exists=True` validation. 

36 """ 

37 missing = tmp_path / "does-not-exist" 

38 

39 result = runner.invoke(app, [str(missing)], env=COMMON_ENV) 

40 

41 assert result.exit_code != 0 

42 mock_main.assert_not_awaited() 

43 

44 

45def test_upload_rejects_file_as_directory_argument( 

46 mock_main: AsyncMock, tmp_path: Path 

47): 

48 """ 

49 Passing a regular file (not a directory) fails validation because `file_okay=False`. 

50 """ 

51 file_path = tmp_path / "report.html" 

52 file_path.write_text("<html></html>") 

53 

54 result = runner.invoke(app, [str(file_path)], env=COMMON_ENV) 

55 

56 assert result.exit_code != 0 

57 mock_main.assert_not_awaited() 

58 

59 

60def test_upload_reads_all_options_from_env_vars(mock_main: AsyncMock, tmp_path: Path): 

61 """ 

62 Every `COVERED_*` env var is consumed and forwarded to `_main` as expected kwargs. 

63 """ 

64 env = { 

65 **COMMON_ENV, 

66 "COVERED_COVERAGE_THRESHOLD": "75.5", 

67 "COVERED_PURGE_CACHE": "true", 

68 } 

69 

70 result = runner.invoke(app, [str(tmp_path)], env=env) 

71 

72 assert result.exit_code == 0, result.stderr 

73 kwargs = mock_main.call_args.kwargs 

74 assert kwargs["directory"] == tmp_path 

75 assert kwargs["api_url"] == "https://api.example.com" 

76 assert kwargs["api_key"] == "test_api_key" 

77 assert kwargs["repo_owner"] == "test_owner" 

78 assert kwargs["repo_name"] == "test_repo" 

79 assert kwargs["commit_sha"] == "test_commit_sha" 

80 assert kwargs["gh_token"] == "test_github_token" 

81 assert kwargs["coverage_threshold"] == 75.5 

82 assert kwargs["purge_cache"] is True 

83 

84 

85def test_upload_cli_flags_override_env_vars(mock_main: AsyncMock, tmp_path: Path): 

86 """ 

87 Explicit `--api-url` (and other flags) take precedence over the corresponding env 

88 vars. 

89 """ 

90 result = runner.invoke( 

91 app, 

92 [str(tmp_path), "--api-url", "https://override.example.com"], 

93 env=COMMON_ENV, 

94 ) 

95 

96 assert result.exit_code == 0, result.stderr 

97 assert mock_main.call_args.kwargs["api_url"] == "https://override.example.com" 

98 

99 

100def test_upload_missing_required_option_fails(mock_main: AsyncMock, tmp_path: Path): 

101 """ 

102 Omitting a required option (e.g. `--api-key` / `COVERED_API_KEY`) exits non-zero 

103 with a clear message. 

104 """ 

105 env = {k: v for k, v in COMMON_ENV.items() if k != "COVERED_API_KEY"} 

106 

107 result = runner.invoke(app, [str(tmp_path)], env=env) 

108 

109 assert result.exit_code != 0 

110 assert "--api-key" in result.stderr 

111 mock_main.assert_not_awaited() 

112 

113 

114def test_upload_default_concurrency_is_50(mock_main: AsyncMock, tmp_path: Path): 

115 """ 

116 When `--concurrency` is not provided, `_main` receives `concurrency=50`. 

117 """ 

118 result = runner.invoke(app, [str(tmp_path)], env=COMMON_ENV) 

119 

120 assert result.exit_code == 0, result.stderr 

121 assert mock_main.call_args.kwargs["concurrency"] == 50 

122 

123 

124def test_upload_default_coverage_threshold_is_100(mock_main: AsyncMock, tmp_path: Path): 

125 """ 

126 `--coverage-threshold` defaults to 100.0. 

127 """ 

128 result = runner.invoke(app, [str(tmp_path)], env=COMMON_ENV) 

129 

130 assert result.exit_code == 0, result.stderr 

131 assert mock_main.call_args.kwargs["coverage_threshold"] == 100.0 

132 

133 

134def test_upload_purge_cache_flag_propagates_true(mock_main: AsyncMock, tmp_path: Path): 

135 """ 

136 `--purge-cache` results in `purge_cache=True` being passed to `_main`. 

137 """ 

138 result = runner.invoke(app, [str(tmp_path), "--purge-cache"], env=COMMON_ENV) 

139 

140 assert result.exit_code == 0, result.stderr 

141 assert mock_main.call_args.kwargs["purge_cache"] is True 

142 

143 

144def test_upload_no_purge_cache_default_is_false(mock_main: AsyncMock, tmp_path: Path): 

145 """ 

146 Without `--purge-cache` (and no env override), `_main` receives `purge_cache=False`. 

147 """ 

148 result = runner.invoke(app, [str(tmp_path)], env=COMMON_ENV) 

149 

150 assert result.exit_code == 0, result.stderr 

151 assert mock_main.call_args.kwargs["purge_cache"] is False 

152 

153 

154def test_upload_help_lists_all_options(): 

155 """ 

156 `--help` output mentions each documented option - sanity check against accidental 

157 removal. 

158 """ 

159 result = runner.invoke(app, ["--help"], env={"COLUMNS": "200"}) 

160 

161 assert result.exit_code == 0 

162 for option in [ 

163 "DIRECTORY", 

164 "--api-url", 

165 "--api-key", 

166 "--concurrency", 

167 "--repo-owner", 

168 "--repo-name", 

169 "--commit-sha", 

170 "--coverage-threshold", 

171 "--gh-token", 

172 "--is-default-branch", 

173 "--purge-cache", 

174 ]: 

175 assert option in result.output, f"missing {option} in help output"