fix: reject plain HTTP repository sources
This commit is contained in:
@@ -52,8 +52,6 @@ pub struct WorkingDirectoryEvidence {
|
||||
pub credential_revision: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub host_trust_revision: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transport_warning: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -856,6 +854,18 @@ impl RuntimeGitMaterializer {
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
fn validate_plain_http_source(
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<(), WorkingDirectoryDiagnostic> {
|
||||
if url::Url::parse(&request.repository.source.uri).is_ok_and(|url| url.scheme() == "http") {
|
||||
return Err(WorkingDirectoryDiagnostic::new(
|
||||
"working_directory_repository_plain_http_unsupported",
|
||||
"plain HTTP Repository sources are not executable; register an HTTPS or SSH source instead",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_request(
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<(), WorkingDirectoryDiagnostic> {
|
||||
@@ -871,11 +881,10 @@ impl RuntimeGitMaterializer {
|
||||
"the configured Repository provider is unsupported",
|
||||
));
|
||||
}
|
||||
Self::validate_plain_http_source(request)?;
|
||||
if matches!(
|
||||
request.repository.source.kind,
|
||||
workspace_api::RepositorySourceKind::Https
|
||||
| workspace_api::RepositorySourceKind::Http
|
||||
| workspace_api::RepositorySourceKind::Ssh
|
||||
workspace_api::RepositorySourceKind::Https | workspace_api::RepositorySourceKind::Ssh
|
||||
) {
|
||||
validate_remote_source_uri(request)?;
|
||||
let materialization = request.materialization.as_ref().ok_or_else(|| {
|
||||
@@ -899,8 +908,7 @@ impl RuntimeGitMaterializer {
|
||||
match request.repository.source.kind {
|
||||
workspace_api::RepositorySourceKind::LocalPath
|
||||
| workspace_api::RepositorySourceKind::File
|
||||
| workspace_api::RepositorySourceKind::Https
|
||||
| workspace_api::RepositorySourceKind::Http => {}
|
||||
| workspace_api::RepositorySourceKind::Https => {}
|
||||
workspace_api::RepositorySourceKind::Ssh => {
|
||||
let ssh = request
|
||||
.materialization
|
||||
@@ -984,6 +992,7 @@ impl RuntimeGitMaterializer {
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<WorkingDirectoryBinding, WorkingDirectoryDiagnostic> {
|
||||
validate_working_directory_id(&working_directory_id)?;
|
||||
Self::validate_plain_http_source(request)?;
|
||||
let request =
|
||||
self.request_with_authorized_repository_access(&working_directory_id, request)?;
|
||||
Self::validate_request(&request)?;
|
||||
@@ -1085,8 +1094,6 @@ impl RuntimeGitMaterializer {
|
||||
host_trust_revision: context
|
||||
.and_then(|value| value.ssh.as_ref())
|
||||
.map(|value| value.host_trust_revision),
|
||||
transport_warning: repository_transport_warning(request.repository.source.kind)
|
||||
.map(str::to_string),
|
||||
},
|
||||
cleanup_target: WorkingDirectoryCleanupTarget {
|
||||
kind: "runtime_git_clone".to_string(),
|
||||
@@ -2393,10 +2400,6 @@ fn validate_ssh_materialization_access(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repository_transport_warning(kind: workspace_api::RepositorySourceKind) -> Option<&'static str> {
|
||||
(kind == workspace_api::RepositorySourceKind::Http).then_some("plain_http_transport")
|
||||
}
|
||||
|
||||
fn validate_remote_source_uri(
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<(), WorkingDirectoryDiagnostic> {
|
||||
@@ -2438,12 +2441,10 @@ fn validate_remote_source_uri(
|
||||
"remote Repository source URI is invalid",
|
||||
)
|
||||
})?;
|
||||
let expected_scheme = match request.repository.source.kind {
|
||||
workspace_api::RepositorySourceKind::Https => "https",
|
||||
workspace_api::RepositorySourceKind::Http => "http",
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if url.scheme() != expected_scheme
|
||||
if request.repository.source.kind != workspace_api::RepositorySourceKind::Https {
|
||||
return Ok(());
|
||||
}
|
||||
if url.scheme() != "https"
|
||||
|| url.host_str().is_none()
|
||||
|| url.password().is_some()
|
||||
|| url.query().is_some()
|
||||
@@ -2576,10 +2577,23 @@ fn read_bounded_command_output(mut reader: impl Read) -> Vec<u8> {
|
||||
captured
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static TEST_REPOSITORY_GIT_INVOCATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn test_repository_git_invocation_count() -> usize {
|
||||
TEST_REPOSITORY_GIT_INVOCATIONS.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
fn run_repository_git_stdout(
|
||||
mut command: Command,
|
||||
source_kind: workspace_api::RepositorySourceKind,
|
||||
) -> Result<String, WorkingDirectoryDiagnostic> {
|
||||
#[cfg(test)]
|
||||
TEST_REPOSITORY_GIT_INVOCATIONS.with(|count| count.set(count.get().saturating_add(1)));
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -2670,6 +2684,9 @@ fn run_repository_git(
|
||||
mut command: Command,
|
||||
code: &'static str,
|
||||
) -> Result<(), WorkingDirectoryDiagnostic> {
|
||||
#[cfg(test)]
|
||||
TEST_REPOSITORY_GIT_INVOCATIONS.with(|count| count.set(count.get().saturating_add(1)));
|
||||
|
||||
tracing::info!(
|
||||
target: "yoi::repository_access",
|
||||
event = "repository_git_operation_started",
|
||||
@@ -4291,16 +4308,54 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut http = request(repo.path());
|
||||
http.repository.source = workspace_api::RepositorySource {
|
||||
kind: workspace_api::RepositorySourceKind::Http,
|
||||
uri: "http://example.test/repo.git".to_string(),
|
||||
};
|
||||
http.repository.source = serde_json::from_value(serde_json::json!({
|
||||
"kind": "http",
|
||||
"uri": "http://example.test/repo.git",
|
||||
}))
|
||||
.unwrap();
|
||||
http.materialization = Some(context(None));
|
||||
RuntimeGitMaterializer::validate_request(&http).unwrap();
|
||||
let git_invocations_before = test_repository_git_invocation_count();
|
||||
let error = materializer
|
||||
.materialize(&worker_ref(3), &http)
|
||||
.expect_err("historical plain HTTP source must fail before Git execution");
|
||||
assert_eq!(
|
||||
repository_transport_warning(http.repository.source.kind),
|
||||
Some("plain_http_transport")
|
||||
error.code,
|
||||
"working_directory_repository_plain_http_unsupported"
|
||||
);
|
||||
assert!(error.message.contains("HTTPS or SSH"));
|
||||
assert_eq!(
|
||||
test_repository_git_invocation_count(),
|
||||
git_invocations_before,
|
||||
"plain HTTP rejection must occur before invoking Git"
|
||||
);
|
||||
|
||||
for (offset, kind) in [
|
||||
workspace_api::RepositorySourceKind::LocalPath,
|
||||
workspace_api::RepositorySourceKind::File,
|
||||
workspace_api::RepositorySourceKind::Https,
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let mut mismatched_http = request(repo.path());
|
||||
mismatched_http.repository.source = workspace_api::RepositorySource {
|
||||
kind,
|
||||
uri: "http://example.test/repo.git".to_string(),
|
||||
};
|
||||
let git_invocations_before = test_repository_git_invocation_count();
|
||||
let error = materializer
|
||||
.materialize(&worker_ref(4 + offset as u64), &mismatched_http)
|
||||
.expect_err("plain HTTP URI must fail regardless of its declared source kind");
|
||||
assert_eq!(
|
||||
error.code,
|
||||
"working_directory_repository_plain_http_unsupported"
|
||||
);
|
||||
assert_eq!(
|
||||
test_repository_git_invocation_count(),
|
||||
git_invocations_before,
|
||||
"mismatched plain HTTP source must fail before invoking Git"
|
||||
);
|
||||
}
|
||||
|
||||
let mut ssh = request(repo.path());
|
||||
ssh.repository.source = workspace_api::RepositorySource {
|
||||
|
||||
@@ -365,23 +365,23 @@ pub fn validate_repository_key(value: &str) -> Result<(), RepositoryKeyError> {
|
||||
///
|
||||
/// Local paths remain distinct from network Git transports so callers cannot
|
||||
/// accidentally treat an unmaterialized remote as a server-local filesystem path.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RepositorySourceKind {
|
||||
LocalPath,
|
||||
File,
|
||||
Ssh,
|
||||
Http,
|
||||
Https,
|
||||
/// A legacy value that could not be classified during migration. It remains
|
||||
/// inspectable but every provider operation must fail closed.
|
||||
/// inspectable but every provider operation must fail closed. Historical
|
||||
/// `http` wire values decode into this non-executable classification.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl RepositorySourceKind {
|
||||
pub const fn is_remote(self) -> bool {
|
||||
matches!(self, Self::Ssh | Self::Http | Self::Https)
|
||||
matches!(self, Self::Ssh | Self::Https)
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
@@ -389,7 +389,6 @@ impl RepositorySourceKind {
|
||||
Self::LocalPath => "local_path",
|
||||
Self::File => "file",
|
||||
Self::Ssh => "ssh",
|
||||
Self::Http => "http",
|
||||
Self::Https => "https",
|
||||
Self::Invalid => "invalid",
|
||||
}
|
||||
@@ -400,14 +399,28 @@ impl RepositorySourceKind {
|
||||
"local_path" => Self::LocalPath,
|
||||
"file" => Self::File,
|
||||
"ssh" => Self::Ssh,
|
||||
"http" => Self::Http,
|
||||
"https" => Self::Https,
|
||||
"invalid" => Self::Invalid,
|
||||
"http" | "invalid" => Self::Invalid,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RepositorySourceKind {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::parse(&value).ok_or_else(|| {
|
||||
serde::de::Error::unknown_variant(
|
||||
&value,
|
||||
&["local_path", "file", "ssh", "https", "invalid"],
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable Repository source identity stored by Workspace authority.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
@@ -3410,6 +3423,27 @@ mod workdir_typescript_tests {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn historical_http_repository_source_kind_decodes_as_invalid_evidence() {
|
||||
let source: RepositorySource = serde_json::from_value(serde_json::json!({
|
||||
"kind": "http",
|
||||
"uri": "http://git.example.test/team/project.git",
|
||||
"revision": 1,
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(source.kind, RepositorySourceKind::Invalid);
|
||||
assert_eq!(
|
||||
serde_json::to_value(source).unwrap()["kind"],
|
||||
serde_json::json!("invalid")
|
||||
);
|
||||
assert_eq!(
|
||||
RepositorySourceKind::parse("http"),
|
||||
Some(RepositorySourceKind::Invalid)
|
||||
);
|
||||
assert!(!RepositorySourceKind::Invalid.is_remote());
|
||||
}
|
||||
|
||||
fn skill_projection() -> SkillProjectionIdentity {
|
||||
SkillProjectionIdentity {
|
||||
config_revision: 42,
|
||||
|
||||
@@ -288,13 +288,12 @@ impl RepositoryRegistryReader {
|
||||
|
||||
fn summary_for_config(&self, repository: &ConfiguredRepository) -> RepositorySummary {
|
||||
let mut diagnostics = Vec::new();
|
||||
if repository.source.kind == workspace_api::RepositorySourceKind::Http {
|
||||
if crate::repository_source::is_plain_http_repository_source(&repository.source) {
|
||||
diagnostics.push(RepositoryDiagnostic {
|
||||
severity: "warning".to_string(),
|
||||
code: "repository_source_insecure_http".to_string(),
|
||||
message:
|
||||
"HTTP Repository source is unencrypted; prefer HTTPS or SSH when available."
|
||||
.to_string(),
|
||||
severity: "error".to_string(),
|
||||
code: "repository_source_plain_http_unsupported".to_string(),
|
||||
message: "Plain HTTP Repository sources are not executable; register an HTTPS or SSH source instead."
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
let git = match repository.provider.as_str() {
|
||||
@@ -607,6 +606,39 @@ mod tests {
|
||||
assert_eq!(projection.diagnostics[0].code, "repository_config_empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_plain_http_source_is_projected_as_non_executable_error() {
|
||||
let source: RepositorySource = serde_json::from_value(serde_json::json!({
|
||||
"kind": "http",
|
||||
"uri": "http://git.example.test/team/project.git",
|
||||
}))
|
||||
.unwrap();
|
||||
let reader = RepositoryRegistryReader::new(vec![ConfiguredRepository {
|
||||
id: "legacy-http".into(),
|
||||
repository_key: "legacy-http".into(),
|
||||
provider: "git".into(),
|
||||
source_fingerprint: crate::repository_source::repository_source_fingerprint(&source),
|
||||
source,
|
||||
source_revision: 1,
|
||||
observed_status: RepositoryObservedStatus::Unverified,
|
||||
observed_at: None,
|
||||
path: None,
|
||||
default_selector: Some("main".into()),
|
||||
}]);
|
||||
|
||||
let projection = reader.list();
|
||||
assert_eq!(
|
||||
projection.items[0].source.kind,
|
||||
workspace_api::RepositorySourceKind::Invalid
|
||||
);
|
||||
let diagnostics = projection.items[0].diagnostics.as_ref().unwrap();
|
||||
assert!(diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.severity == "error"
|
||||
&& diagnostic.code == "repository_source_plain_http_unsupported"
|
||||
&& diagnostic.message.contains("HTTPS or SSH")
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_source_is_visible_but_local_provider_operations_fail_closed() {
|
||||
let source = RepositorySource {
|
||||
|
||||
@@ -136,6 +136,18 @@ pub fn project_repository_access_state(
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_repository_access_source(
|
||||
repository_key: &str,
|
||||
source: &workspace_api::RepositorySource,
|
||||
) -> Result<()> {
|
||||
if crate::repository_source::is_plain_http_repository_source(source) {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"repository_source_plain_http_unsupported: Repository `{repository_key}` uses unsupported plain HTTP; register an HTTPS or SSH source instead"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn repository_ssh_endpoint(
|
||||
repository_key: &str,
|
||||
repository_uri: &str,
|
||||
@@ -200,6 +212,7 @@ fn project_repository_access_evaluation(
|
||||
let repository = store
|
||||
.get_repository_by_key(workspace_id, &repository_key)?
|
||||
.ok_or_else(|| Error::InvalidInput(format!("unknown Repository `{repository_key}`")))?;
|
||||
validate_repository_access_source(&repository_key, &repository.source)?;
|
||||
if repository.source.kind != workspace_api::RepositorySourceKind::Ssh {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
"Repository `{repository_key}` is not an ssh:// Repository"
|
||||
@@ -1956,6 +1969,30 @@ mod tests {
|
||||
assert!(!contribution.source.contains("secret_ref"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_config_projection_rejects_legacy_plain_http_repository_source() {
|
||||
let source: RepositorySource = serde_json::from_value(serde_json::json!({
|
||||
"kind": "http",
|
||||
"uri": "http://git.example.test/team/project.git",
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let error = validate_repository_access_source("remote", &source).unwrap_err();
|
||||
assert!(error.to_string().contains("unsupported plain HTTP"));
|
||||
assert!(error.to_string().contains("HTTPS or SSH"));
|
||||
|
||||
let mismatched = workspace_api::RepositorySource {
|
||||
kind: workspace_api::RepositorySourceKind::Https,
|
||||
uri: "http://git.example.test/team/project.git".to_string(),
|
||||
};
|
||||
let error = validate_repository_access_source("remote", &mismatched).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("repository_source_plain_http_unsupported")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn master_key_is_external_and_stable() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -76,18 +76,19 @@ pub fn parse_repository_source(value: &str) -> Result<RepositorySource> {
|
||||
require_remote_host_and_path(&parsed)?;
|
||||
RepositorySourceKind::Ssh
|
||||
}
|
||||
"http" | "https" => {
|
||||
"http" => {
|
||||
return Err(Error::InvalidInput(
|
||||
"repository_source_plain_http_unsupported: plain HTTP Repository sources are not supported; use HTTPS or SSH".to_string(),
|
||||
));
|
||||
}
|
||||
"https" => {
|
||||
if !parsed.username().is_empty() {
|
||||
return Err(Error::InvalidInput(
|
||||
"HTTP repository URI must not contain user information".to_string(),
|
||||
"HTTPS repository URI must not contain user information".to_string(),
|
||||
));
|
||||
}
|
||||
require_remote_host_and_path(&parsed)?;
|
||||
if parsed.scheme() == "http" {
|
||||
RepositorySourceKind::Http
|
||||
} else {
|
||||
RepositorySourceKind::Https
|
||||
}
|
||||
RepositorySourceKind::Https
|
||||
}
|
||||
scheme => {
|
||||
return Err(Error::InvalidInput(format!(
|
||||
@@ -111,6 +112,10 @@ pub fn classify_legacy_repository_source(value: &str) -> RepositorySource {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_plain_http_repository_source(source: &RepositorySource) -> bool {
|
||||
Url::parse(&source.uri).is_ok_and(|url| url.scheme() == "http")
|
||||
}
|
||||
|
||||
pub fn repository_source_fingerprint(source: &RepositorySource) -> String {
|
||||
let payload = serde_json::to_vec(source).expect("Repository source serializes");
|
||||
let mut hasher = Sha256::new();
|
||||
@@ -172,7 +177,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_local_file_ssh_http_and_https_sources_without_io() {
|
||||
fn parses_local_file_ssh_and_https_sources_without_io() {
|
||||
let cases = [
|
||||
("/runtime/repos/project", RepositorySourceKind::LocalPath),
|
||||
("file:///runtime/repos/project", RepositorySourceKind::File),
|
||||
@@ -184,10 +189,6 @@ mod tests {
|
||||
"git@example.test:org/project.git",
|
||||
RepositorySourceKind::Ssh,
|
||||
),
|
||||
(
|
||||
"http://git.test/org/project.git",
|
||||
RepositorySourceKind::Http,
|
||||
),
|
||||
(
|
||||
"https://git.test/org/project.git",
|
||||
RepositorySourceKind::Https,
|
||||
@@ -198,6 +199,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_plain_http_with_secure_transport_guidance() {
|
||||
for source in [
|
||||
"http://git.test/org/project.git",
|
||||
"http://localhost/org/project.git",
|
||||
"http://127.0.0.1/org/project.git",
|
||||
] {
|
||||
let error = parse_repository_source(source).expect_err("plain HTTP must fail closed");
|
||||
assert!(error.to_string().contains("plain HTTP Repository sources"));
|
||||
assert!(error.to_string().contains("HTTPS or SSH"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_plain_http_is_preserved_only_as_invalid_evidence() {
|
||||
let source = classify_legacy_repository_source("http://git.test/org/project.git");
|
||||
assert_eq!(source.kind, RepositorySourceKind::Invalid);
|
||||
assert_eq!(source.uri, "http://git.test/org/project.git");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_relative_unsupported_and_credential_bearing_sources() {
|
||||
for source in [
|
||||
|
||||
@@ -364,7 +364,6 @@ fn repository_local_path(source: &workspace_api::RepositorySource) -> Option<Pat
|
||||
.ok()
|
||||
.and_then(|uri| uri.to_file_path().ok()),
|
||||
workspace_api::RepositorySourceKind::Ssh
|
||||
| workspace_api::RepositorySourceKind::Http
|
||||
| workspace_api::RepositorySourceKind::Https
|
||||
| workspace_api::RepositorySourceKind::Invalid => None,
|
||||
}
|
||||
@@ -10818,9 +10817,8 @@ async fn create_workspace_working_directory(
|
||||
"local_path" => workspace_api::RepositorySourceKind::LocalPath,
|
||||
"file" => workspace_api::RepositorySourceKind::File,
|
||||
"https" => workspace_api::RepositorySourceKind::Https,
|
||||
"http" => workspace_api::RepositorySourceKind::Http,
|
||||
"http" | "invalid" => workspace_api::RepositorySourceKind::Invalid,
|
||||
"ssh" => workspace_api::RepositorySourceKind::Ssh,
|
||||
"invalid" => workspace_api::RepositorySourceKind::Invalid,
|
||||
_ => {
|
||||
return Err(settings_bad_request(
|
||||
"working_directory_repository_source_invalid",
|
||||
@@ -22275,6 +22273,22 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(replayed.status(), StatusCode::OK);
|
||||
|
||||
let plain_http = app
|
||||
.clone()
|
||||
.oneshot(create_repository(serde_json::json!({
|
||||
"repository_key": "insecure-http",
|
||||
"source": "http://git.example.test/team/project.git",
|
||||
"default_ref": "main"
|
||||
})))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(plain_http.status(), StatusCode::BAD_REQUEST);
|
||||
let plain_http_body = to_bytes(plain_http.into_body(), usize::MAX).await.unwrap();
|
||||
let plain_http_body = String::from_utf8_lossy(&plain_http_body);
|
||||
assert!(plain_http_body.contains("repository_source_plain_http_unsupported"));
|
||||
assert!(plain_http_body.contains("plain HTTP Repository sources"));
|
||||
assert!(plain_http_body.contains("HTTPS or SSH"));
|
||||
|
||||
let conflict = app
|
||||
.clone()
|
||||
.oneshot(create_repository(serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user