fix: reject conflicting repository import intent
This commit is contained in:
@@ -18175,6 +18175,47 @@ mod tests {
|
|||||||
SqliteWorkspaceStore::open(&config.database_path).unwrap()
|
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]
|
#[tokio::test]
|
||||||
async fn server_router_serves_workspace_chooser_before_first_workspace_exists() {
|
async fn server_router_serves_workspace_chooser_before_first_workspace_exists() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -2197,38 +2197,67 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||||||
|
|
||||||
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
|
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
|
||||||
validate_repository_record_identity(record)?;
|
validate_repository_record_identity(record)?;
|
||||||
self.with_conn(|conn| {
|
self.with_conn_mut(|conn| {
|
||||||
conn.execute(
|
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
|
||||||
r#"INSERT INTO repositories (
|
let existing = tx
|
||||||
workspace_id, repository_id, repository_key, kind, provider, uri,
|
.query_row(
|
||||||
source_kind, source_uri, default_ref, source_revision,
|
r#"SELECT workspace_id, repository_id, repository_key, kind, provider,
|
||||||
source_fingerprint, observed_status, observed_at, created_at, updated_at
|
source_kind, source_uri, default_ref, source_revision,
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
|
source_fingerprint, observed_status, observed_at, created_at, updated_at
|
||||||
ON CONFLICT(workspace_id, repository_key) DO UPDATE SET
|
FROM repositories
|
||||||
kind = excluded.kind,
|
WHERE workspace_id = ?1 AND repository_key = ?2"#,
|
||||||
provider = excluded.provider,
|
params![record.workspace_id, record.repository_key],
|
||||||
default_ref = excluded.default_ref,
|
read_repository_record,
|
||||||
observed_status = excluded.observed_status,
|
)
|
||||||
observed_at = excluded.observed_at,
|
.optional()?;
|
||||||
updated_at = excluded.updated_at"#,
|
if let Some(existing) = existing {
|
||||||
params![
|
if !repository_registration_intent_matches(&existing, record) {
|
||||||
record.workspace_id,
|
return Err(Error::WorkspaceConfigConflict(format!(
|
||||||
record.repository_id,
|
"Repository key `{}` already exists with different registration intent",
|
||||||
record.repository_key,
|
record.repository_key
|
||||||
record.kind,
|
)));
|
||||||
record.provider,
|
}
|
||||||
record.source.uri,
|
tx.execute(
|
||||||
record.source.kind.as_str(),
|
r#"UPDATE repositories
|
||||||
record.source.uri,
|
SET observed_status = ?3,
|
||||||
record.default_ref,
|
observed_at = ?4,
|
||||||
record.source_revision,
|
updated_at = ?5
|
||||||
record.source_fingerprint,
|
WHERE workspace_id = ?1 AND repository_id = ?2"#,
|
||||||
record.observed_status.as_str(),
|
params![
|
||||||
record.observed_at,
|
record.workspace_id,
|
||||||
record.created_at,
|
existing.repository_id,
|
||||||
record.updated_at,
|
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(())
|
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<()> {
|
fn validate_repository_record_identity(record: &RepositoryRecord) -> Result<()> {
|
||||||
workspace_api::validate_repository_key(&record.repository_key)
|
workspace_api::validate_repository_key(&record.repository_key)
|
||||||
.map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?;
|
.map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user