fix: reject conflicting repository import intent
This commit is contained in:
@@ -18175,6 +18175,47 @@ mod tests {
|
||||
SqliteWorkspaceStore::open(&config.database_path).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn configured_repository_import_rejects_changed_source_intent() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut config = test_server_config(dir.path());
|
||||
let store = test_control_store(&config);
|
||||
seed_test_registered_workspace(&store, &config)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
import_configured_repositories(&store, &config).unwrap();
|
||||
let persisted = store
|
||||
.get_repository_by_key(TEST_WORKSPACE_ID, "test-repository")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(persisted.source, config.repositories[0].source);
|
||||
|
||||
let changed_source = workspace_api::RepositorySource {
|
||||
kind: workspace_api::RepositorySourceKind::LocalPath,
|
||||
uri: dir.path().join("different-source").display().to_string(),
|
||||
};
|
||||
config.repositories[0].source_fingerprint =
|
||||
crate::repository_source::repository_source_fingerprint(&changed_source);
|
||||
config.repositories[0].source = changed_source;
|
||||
|
||||
let error = import_configured_repositories(&store, &config).unwrap_err();
|
||||
assert!(
|
||||
matches!(error, crate::Error::WorkspaceConfigConflict(_)),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
let after_rejected_import = store
|
||||
.get_repository_by_key(TEST_WORKSPACE_ID, "test-repository")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(after_rejected_import.repository_id, persisted.repository_id);
|
||||
assert_eq!(after_rejected_import.source, persisted.source);
|
||||
assert_eq!(
|
||||
after_rejected_import.source_fingerprint,
|
||||
persisted.source_fingerprint
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn server_router_serves_workspace_chooser_before_first_workspace_exists() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -2197,38 +2197,67 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||
|
||||
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
|
||||
validate_repository_record_identity(record)?;
|
||||
self.with_conn(|conn| {
|
||||
conn.execute(
|
||||
r#"INSERT INTO repositories (
|
||||
workspace_id, repository_id, repository_key, kind, provider, uri,
|
||||
source_kind, source_uri, default_ref, source_revision,
|
||||
source_fingerprint, observed_status, observed_at, created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
|
||||
ON CONFLICT(workspace_id, repository_key) DO UPDATE SET
|
||||
kind = excluded.kind,
|
||||
provider = excluded.provider,
|
||||
default_ref = excluded.default_ref,
|
||||
observed_status = excluded.observed_status,
|
||||
observed_at = excluded.observed_at,
|
||||
updated_at = excluded.updated_at"#,
|
||||
params![
|
||||
record.workspace_id,
|
||||
record.repository_id,
|
||||
record.repository_key,
|
||||
record.kind,
|
||||
record.provider,
|
||||
record.source.uri,
|
||||
record.source.kind.as_str(),
|
||||
record.source.uri,
|
||||
record.default_ref,
|
||||
record.source_revision,
|
||||
record.source_fingerprint,
|
||||
record.observed_status.as_str(),
|
||||
record.observed_at,
|
||||
record.created_at,
|
||||
record.updated_at,
|
||||
],
|
||||
)?;
|
||||
self.with_conn_mut(|conn| {
|
||||
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||
let existing = tx
|
||||
.query_row(
|
||||
r#"SELECT workspace_id, repository_id, repository_key, kind, provider,
|
||||
source_kind, source_uri, default_ref, source_revision,
|
||||
source_fingerprint, observed_status, observed_at, created_at, updated_at
|
||||
FROM repositories
|
||||
WHERE workspace_id = ?1 AND repository_key = ?2"#,
|
||||
params![record.workspace_id, record.repository_key],
|
||||
read_repository_record,
|
||||
)
|
||||
.optional()?;
|
||||
if let Some(existing) = existing {
|
||||
if !repository_registration_intent_matches(&existing, record) {
|
||||
return Err(Error::WorkspaceConfigConflict(format!(
|
||||
"Repository key `{}` already exists with different registration intent",
|
||||
record.repository_key
|
||||
)));
|
||||
}
|
||||
tx.execute(
|
||||
r#"UPDATE repositories
|
||||
SET observed_status = ?3,
|
||||
observed_at = ?4,
|
||||
updated_at = ?5
|
||||
WHERE workspace_id = ?1 AND repository_id = ?2"#,
|
||||
params![
|
||||
record.workspace_id,
|
||||
existing.repository_id,
|
||||
record.observed_status.as_str(),
|
||||
record.observed_at,
|
||||
record.updated_at,
|
||||
],
|
||||
)?;
|
||||
} else {
|
||||
tx.execute(
|
||||
r#"INSERT INTO repositories (
|
||||
workspace_id, repository_id, repository_key, kind, provider, uri,
|
||||
source_kind, source_uri, default_ref, source_revision,
|
||||
source_fingerprint, observed_status, observed_at, created_at, updated_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)"#,
|
||||
params![
|
||||
record.workspace_id,
|
||||
record.repository_id,
|
||||
record.repository_key,
|
||||
record.kind,
|
||||
record.provider,
|
||||
record.source.uri,
|
||||
record.source.kind.as_str(),
|
||||
record.source.uri,
|
||||
record.default_ref,
|
||||
record.source_revision,
|
||||
record.source_fingerprint,
|
||||
record.observed_status.as_str(),
|
||||
record.observed_at,
|
||||
record.created_at,
|
||||
record.updated_at,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -5358,6 +5387,20 @@ fn read_workspace_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<WorkspaceR
|
||||
})
|
||||
}
|
||||
|
||||
fn repository_registration_intent_matches(
|
||||
existing: &RepositoryRecord,
|
||||
requested: &RepositoryRecord,
|
||||
) -> bool {
|
||||
existing.workspace_id == requested.workspace_id
|
||||
&& existing.repository_key == requested.repository_key
|
||||
&& existing.kind == requested.kind
|
||||
&& existing.provider == requested.provider
|
||||
&& existing.source == requested.source
|
||||
&& existing.default_ref == requested.default_ref
|
||||
&& existing.source_revision == requested.source_revision
|
||||
&& existing.source_fingerprint == requested.source_fingerprint
|
||||
}
|
||||
|
||||
fn validate_repository_record_identity(record: &RepositoryRecord) -> Result<()> {
|
||||
workspace_api::validate_repository_key(&record.repository_key)
|
||||
.map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?;
|
||||
|
||||
Reference in New Issue
Block a user