Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions crates/bug/src/adapters/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ struct BugResponse {
/// Handler user id.
#[serde(skip_serializing_if = "Option::is_none")]
handler: Option<String>,
/// Markdown description body (defect details).
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Last mutator's user id (stamped on every mutation).
#[serde(skip_serializing_if = "Option::is_none")]
updated_by: Option<String>,
Expand All @@ -131,6 +134,7 @@ impl From<Bug> for BugResponse {
created_by: b.created_by,
severity: b.severity,
handler: b.handler,
description: b.description,
updated_by: b.updated_by,
updated_at: b.updated_at,
custom_fields: b.custom_fields,
Expand All @@ -148,6 +152,8 @@ struct CreateBugRequest {
severity: Option<String>,
/// Handler user id (optional).
handler: Option<String>,
/// Markdown description body (optional).
description: Option<String>,
/// Custom field values, key → string value (max 32 keys, key ≤ 64 chars, value ≤ 2000 chars).
#[serde(default)]
custom_fields: BTreeMap<String, String>,
Expand All @@ -171,6 +177,7 @@ async fn create_bug(
Some(&user.user_id),
req.severity.as_deref(),
req.handler.as_deref(),
req.description.as_deref(),
&req.custom_fields,
)
.await
Expand Down Expand Up @@ -261,6 +268,8 @@ struct UpdateBugRequest {
severity: Option<String>,
/// Handler user id; absent clears.
handler: Option<String>,
/// Markdown description body; absent keeps the current value.
description: Option<String>,
}

/// Updates title/severity/handler and stamps updated_by/updated_at with the caller.
Expand All @@ -281,6 +290,7 @@ async fn update_bug(
req.title.as_deref(),
req.severity.as_deref(),
req.handler.as_deref(),
req.description.as_deref(),
Some(&user.user_id),
)
.await
Expand Down
3 changes: 3 additions & 0 deletions crates/bug/src/adapters/in_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl BugRepository for InMemoryBugRepository {
created_by: new_bug.created_by.clone(),
severity: new_bug.severity.clone(),
handler: new_bug.handler.clone(),
description: new_bug.description.clone(),
updated_by: new_bug.created_by.clone(),
updated_at: Some(tick(seq)),
custom_fields: new_bug.custom_fields.clone(),
Expand Down Expand Up @@ -136,6 +137,7 @@ impl BugRepository for InMemoryBugRepository {
title: &str,
severity: Option<&str>,
handler: Option<&str>,
description: Option<&str>,
operator: Option<&str>,
) -> Result<Option<Bug>, RepoError> {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
Expand All @@ -145,6 +147,7 @@ impl BugRepository for InMemoryBugRepository {
b.title = title.to_string();
b.severity = severity.map(str::to_string);
b.handler = handler.map(str::to_string);
b.description = description.map(str::to_string);
b.updated_by = operator.map(str::to_string);
b.updated_at = Some(tick(seq));
b.clone()
Expand Down
19 changes: 14 additions & 5 deletions crates/bug/src/adapters/pg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn json_to_fields(v: &serde_json::Value) -> BTreeMap<String, String> {
}

const BUG_COLS: &str = "id, project_id, title, status, deleted, created_at, created_by, \
severity, handler, updated_by, updated_at::text AS updated_at, custom_fields";
severity, handler, description, updated_by, updated_at::text AS updated_at, custom_fields";

fn row_to_bug(row: &sqlx::postgres::PgRow) -> Result<Bug, RepoError> {
let custom: serde_json::Value = row.try_get("custom_fields").map_err(map_err)?;
Expand All @@ -59,6 +59,7 @@ fn row_to_bug(row: &sqlx::postgres::PgRow) -> Result<Bug, RepoError> {
created_by: row.try_get("created_by").map_err(map_err)?,
severity: row.try_get("severity").map_err(map_err)?,
handler: row.try_get("handler").map_err(map_err)?,
description: row.try_get("description").map_err(map_err)?,
updated_by: row.try_get("updated_by").map_err(map_err)?,
updated_at: row.try_get("updated_at").map_err(map_err)?,
custom_fields: json_to_fields(&custom),
Expand Down Expand Up @@ -108,8 +109,8 @@ impl BugRepository for PgBugRepository {
// updated_by/updated_at seed from the creator so the audit pair is never empty.
let row = sqlx::query(&format!(
"INSERT INTO ms_bug (project_id, title, status, created_by, custom_fields, \
severity, handler, updated_by, updated_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $4, now()) RETURNING {BUG_COLS}"
severity, handler, description, updated_by, updated_at) \
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $4, now()) RETURNING {BUG_COLS}"
))
.bind(&new_bug.project_id)
.bind(&new_bug.title)
Expand All @@ -118,6 +119,7 @@ impl BugRepository for PgBugRepository {
.bind(fields_to_json(&new_bug.custom_fields))
.bind(&new_bug.severity)
.bind(&new_bug.handler)
.bind(&new_bug.description)
.fetch_one(&self.pool)
.await
.map_err(map_err)?;
Expand Down Expand Up @@ -171,10 +173,12 @@ impl BugRepository for PgBugRepository {
title: &str,
severity: Option<&str>,
handler: Option<&str>,
description: Option<&str>,
operator: Option<&str>,
) -> Result<Option<Bug>, RepoError> {
let row = sqlx::query(&format!(
"UPDATE ms_bug SET title = $2, severity = $3, handler = $4, \
description = COALESCE($6, description), \
updated_by = $5, updated_at = now() \
WHERE id = $1 AND deleted = false RETURNING {BUG_COLS}"
))
Expand All @@ -183,6 +187,7 @@ impl BugRepository for PgBugRepository {
.bind(severity)
.bind(handler)
.bind(operator)
.bind(description)
.fetch_optional(&self.pool)
.await
.map_err(map_err)?;
Expand Down Expand Up @@ -368,15 +373,19 @@ mod tests {

// Meta update replaces severity/handler and stamps the operator.
let updated = repo
.update_meta(&bug.id, "login crash (modified)", Some("P0"), None, Some("dave"))
.update_meta(&bug.id, "login crash (modified)", Some("P0"), None, None, Some("dave"))
.await
.expect("meta")
.expect("some");
assert_eq!(updated.title, "login crash (modified)");
assert_eq!(updated.severity.as_deref(), Some("P0"));
assert_eq!(updated.handler, None);
assert_eq!(updated.updated_by.as_deref(), Some("dave"));
assert!(repo.update_meta("ghost", "x", None, None, None).await.expect("meta").is_none());
assert!(repo
.update_meta("ghost", "x", None, None, None, None)
.await
.expect("meta")
.is_none());

// Full replacement; empty map clears all.
repo.set_custom_fields(&bug.id, &cf(&[("env", "prod")]), Some("erin"))
Expand Down
11 changes: 10 additions & 1 deletion crates/bug/src/application/change_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,16 @@ mod tests {
async fn seed_bug(repo: &InMemoryBugRepository) -> String {
let create = CreateBugUseCase::new(Arc::new(repo.clone()));
create
.execute("p1", "boom", "NEW", None, None, None, &std::collections::BTreeMap::new())
.execute(
"p1",
"boom",
"NEW",
None,
None,
None,
None,
&std::collections::BTreeMap::new(),
)
.await
.expect("seed")
.id
Expand Down
26 changes: 18 additions & 8 deletions crates/bug/src/application/create_bug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ impl CreateBugUseCase {
created_by: Option<&str>,
severity: Option<&str>,
handler: Option<&str>,
description: Option<&str>,
custom_fields: &BTreeMap<String, String>,
) -> Result<Bug, CreateBugError> {
let custom_fields = normalize_custom_fields(custom_fields)?;
Expand All @@ -48,6 +49,7 @@ impl CreateBugUseCase {
.with_created_by(created_by)
.with_severity(severity)
.with_handler(normalize_handler(handler))
.with_description(description.map(|s| s.to_string()))
.with_custom_fields(custom_fields);

let flow = self.repo.status_flow(project_id).await?;
Expand All @@ -71,7 +73,7 @@ mod tests {
let repo = InMemoryBugRepository::with_default_flow("p1");
let uc = CreateBugUseCase::new(Arc::new(repo));
let bug = uc
.execute("p1", "login crash", "NEW", Some("alice"), None, None, &BTreeMap::new())
.execute("p1", "login crash", "NEW", Some("alice"), None, None, None, &BTreeMap::new())
.await
.expect("ok");
assert_eq!(bug.status, "NEW");
Expand All @@ -95,6 +97,7 @@ mod tests {
None,
Some(" p0 "),
Some(" bob "),
None,
&BTreeMap::new(),
)
.await
Expand All @@ -108,7 +111,7 @@ mod tests {
let repo = InMemoryBugRepository::with_default_flow("p1");
let uc = CreateBugUseCase::new(Arc::new(repo));
let err = uc
.execute("p1", "x", "NEW", None, Some("SEV1"), None, &BTreeMap::new())
.execute("p1", "x", "NEW", None, Some("SEV1"), None, None, &BTreeMap::new())
.await
.unwrap_err();
assert_eq!(err, CreateBugError::Validation(BugError::InvalidSeverity("SEV1".into())));
Expand All @@ -119,12 +122,15 @@ mod tests {
let repo = InMemoryBugRepository::with_default_flow("p1");
let uc = CreateBugUseCase::new(Arc::new(repo));
let raw = BTreeMap::from([(" severity ".to_string(), "P0".to_string())]);
let bug = uc.execute("p1", "login crash", "NEW", None, None, None, &raw).await.expect("ok");
let bug =
uc.execute("p1", "login crash", "NEW", None, None, None, None, &raw).await.expect("ok");
assert_eq!(bug.custom_fields, BTreeMap::from([("severity".to_string(), "P0".to_string())]));
// Blank key is a validation error.
let bad = BTreeMap::from([(" ".to_string(), "v".to_string())]);
assert_eq!(
uc.execute("p1", "crashed again", "NEW", None, None, None, &bad).await.unwrap_err(),
uc.execute("p1", "crashed again", "NEW", None, None, None, None, &bad)
.await
.unwrap_err(),
CreateBugError::Validation(BugError::EmptyCustomFieldKey)
);
}
Expand All @@ -133,17 +139,21 @@ mod tests {
async fn rejects_unknown_initial_status() {
let repo = InMemoryBugRepository::with_default_flow("p1");
let uc = CreateBugUseCase::new(Arc::new(repo));
let err =
uc.execute("p1", "x", "GHOST", None, None, None, &BTreeMap::new()).await.unwrap_err();
let err = uc
.execute("p1", "x", "GHOST", None, None, None, None, &BTreeMap::new())
.await
.unwrap_err();
assert_eq!(err, CreateBugError::Validation(BugError::UnknownStatus("GHOST".into())));
}

#[tokio::test]
async fn rejects_blank_title() {
let repo = InMemoryBugRepository::with_default_flow("p1");
let uc = CreateBugUseCase::new(Arc::new(repo));
let err =
uc.execute("p1", " ", "NEW", None, None, None, &BTreeMap::new()).await.unwrap_err();
let err = uc
.execute("p1", " ", "NEW", None, None, None, None, &BTreeMap::new())
.await
.unwrap_err();
assert_eq!(err, CreateBugError::Validation(BugError::EmptyTitle));
}
}
2 changes: 1 addition & 1 deletion crates/bug/src/application/custom_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ mod tests {

async fn seed_bug(repo: &InMemoryBugRepository) -> String {
CreateBugUseCase::new(Arc::new(repo.clone()))
.execute("p1", "boom", "NEW", None, None, None, &fields(&[("severity", "P1")]))
.execute("p1", "boom", "NEW", None, None, None, None, &fields(&[("severity", "P1")]))
.await
.expect("seed")
.id
Expand Down
Loading
Loading