use serde_json::{Value, json};
use crate::common::{Sandbox, stderr, stdout};
use crate::fixtures::{
GitHubBoardFields, document, github_projects_with_board, github_projects_with_board_failing,
};
fn configured() -> (Sandbox, GitHubBoardFields) {
let sandbox = Sandbox::new();
let (config, board) = github_projects_with_board(&sandbox);
sandbox.project_document(&document(&json!({"board": {
"plugin": "github-projects", "config": config
}})));
(sandbox, board)
}
fn report(sandbox: &Sandbox, arguments: &[&str]) -> Value {
let output = sandbox
.command()
.args(arguments)
.assert()
.success()
.get_output()
.clone();
serde_json::from_str(&stdout(&output)).expect("a JSON fields report")
}
fn mutations(board: &GitHubBoardFields) -> Vec<(String, Value)> {
board
.served()
.into_iter()
.filter(|(query, _)| query.trim_start().starts_with("mutation"))
.collect()
}
fn values(sandbox: &Sandbox) -> Value {
report(sandbox, &["--json", "task", "list"])["items"]
.as_array()
.expect("a page of tasks")
.iter()
.map(|task| json!([task["id"], task["item"]["status"], task["item"]["priority"]]))
.collect()
}
#[test]
fn a_board_already_set_up_is_a_plan_that_writes_nothing_and_an_unchanged_apply() {
let (sandbox, board) = configured();
let status_before = board.status_options();
let planned = report(&sandbox, &["--json", "sources", "fields", "board"]);
assert_eq!(planned["source"], "board");
let fields = planned["fields"].as_array().expect("one entry per field");
assert_eq!(fields.len(), 2);
assert_eq!(fields[0]["field"], "Status");
assert_eq!(fields[1]["field"], "Priority");
for field in fields {
assert_eq!(field["exists"], true);
assert_eq!(field["missing"], json!([]));
assert_eq!(field["outcome"], "planned");
}
assert_eq!(fields[0]["existing"], json!(status_before));
assert_eq!(
fields[1]["existing"],
json!(board.priority_options().expect("the field"))
);
let applied = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
for field in applied["fields"].as_array().expect("fields") {
assert_eq!(field["outcome"], "unchanged");
}
assert!(mutations(&board).is_empty(), "{:?}", mutations(&board));
let text = sandbox
.command()
.args(["sources", "fields", "board"])
.assert()
.success()
.get_output()
.clone();
assert_eq!(
stdout(&text),
"board: missing configured Status options: none\n\
board: missing configured Priority options: none\n"
);
}
#[test]
fn a_missing_priority_field_is_planned_then_created_with_the_mapped_options_in_order() {
let (sandbox, board) = configured();
board.without_priority_field();
let status_before = board.status_options();
let values_before = values(&sandbox);
let planned = report(&sandbox, &["--json", "sources", "fields", "board"]);
assert_eq!(
planned["fields"][1],
json!({"field": "Priority", "exists": false,
"missing": ["Urgent", "High", "Medium", "Low"],
"outcome": "planned", "existing": []})
);
assert!(mutations(&board).is_empty(), "a plan writes nothing");
let text = sandbox
.command()
.args(["sources", "fields", "board"])
.assert()
.success()
.get_output()
.clone();
assert!(
stdout(&text)
.contains("board: no Priority field; would create it with: Urgent, High, Medium, Low"),
"{}",
stdout(&text)
);
let applied = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.success()
.get_output()
.clone();
assert_eq!(
stdout(&applied),
"board: missing configured Status options: none\n\
board: created the Priority field with: Urgent, High, Medium, Low\n"
);
let sent = mutations(&board);
assert_eq!(sent.len(), 1, "{sent:?}");
assert!(sent[0].0.contains("createProjectV2Field(input:$input)"));
assert_eq!(
sent[0].1["input"],
json!({"projectId": "PVT-board", "dataType": "SINGLE_SELECT", "name": "Priority",
"singleSelectOptions": [
{"name": "Urgent", "color": "GRAY", "description": ""},
{"name": "High", "color": "GRAY", "description": ""},
{"name": "Medium", "color": "GRAY", "description": ""},
{"name": "Low", "color": "GRAY", "description": ""}]})
);
let names: Vec<Value> = board
.priority_options()
.expect("the field was created")
.iter()
.map(|option| option["name"].clone())
.collect();
assert_eq!(
names,
[
json!("Urgent"),
json!("High"),
json!("Medium"),
json!("Low")
]
);
assert_eq!(board.status_options(), status_before, "Status is untouched");
assert_eq!(values(&sandbox), values_before, "no item's values moved");
let again = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
assert_eq!(again["fields"][1]["outcome"], "unchanged");
report(
&sandbox,
&["--json", "task", "priority", "set", "board:T-2", "medium"],
);
assert_eq!(board.priority("T-2").as_deref(), Some("Medium"));
}
#[test]
fn a_missing_option_of_either_field_is_added_with_every_existing_option_and_value_preserved() {
let (sandbox, board) = configured();
board.without_priority_option("Medium");
board.without_option("Queued");
let status_before = board.status_options();
let priority_before = board.priority_options().expect("the field");
let values_before = values(&sandbox);
let planned = sandbox
.command()
.args(["sources", "fields", "board"])
.assert()
.success()
.get_output()
.clone();
assert_eq!(
stdout(&planned),
"board: missing configured Status options: Queued\n\
board: missing configured Priority options: Medium\n"
);
let applied = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
assert_eq!(applied["fields"][0]["missing"], json!(["Queued"]));
assert_eq!(applied["fields"][0]["outcome"], "applied");
assert_eq!(applied["fields"][1]["missing"], json!(["Medium"]));
assert_eq!(applied["fields"][1]["outcome"], "applied");
assert_eq!(applied["fields"][1]["existing"], json!(priority_before));
let sent = mutations(&board);
assert_eq!(sent.len(), 2, "{sent:?}");
for (query, variables) in &sent {
assert!(
query.contains("updateProjectV2Field(input:$input)"),
"{query}"
);
let (before, added) = if variables["input"]["fieldId"] == "FIELD-status" {
(&status_before, "Queued")
} else {
assert_eq!(variables["input"]["fieldId"], "FIELD-priority");
(&priority_before, "Medium")
};
let options = variables["input"]["singleSelectOptions"]
.as_array()
.expect("the whole option list");
assert_eq!(&options[..before.len()], &before[..]);
assert_eq!(
options[before.len()..],
[json!({"name": added, "color": "GRAY", "description": ""})]
);
}
let after = board.priority_options().expect("the field");
assert!(priority_before.iter().all(|option| after.contains(option)));
assert!(
status_before
.iter()
.all(|option| board.status_options().contains(option))
);
assert_eq!(values(&sandbox), values_before, "no item's values moved");
}
#[test]
fn the_human_rendering_says_what_an_apply_added_and_verified() {
let (sandbox, board) = configured();
board.without_priority_option("Low");
let applied = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.success()
.get_output()
.clone();
assert_eq!(
stdout(&applied),
"board: missing configured Status options: none\n\
board: added and verified Priority options: Low\n"
);
assert!(
board
.priority_options()
.expect("the field")
.iter()
.any(|option| option["name"] == "Low")
);
}
#[test]
fn a_second_field_failing_after_the_first_changed_is_refused_with_the_recovery_data() {
let sandbox = Sandbox::new();
let (config, board) =
github_projects_with_board_failing(&sandbox, &["createProjectV2Field(input:$input)"]);
sandbox.project_document(&document(&json!({"board": {
"plugin": "github-projects", "config": config
}})));
board.without_option("Queued");
board.without_priority_field();
let output = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.failure()
.get_output()
.clone();
let said = stderr(&output);
assert!(
said.contains(
"changed the Status field and then failed on the Priority field, which it may have \
changed part way"
) && said.contains("createProjectV2Field")
&& said.contains("the pre-write item assignments are:")
&& said.contains("\"Status\"")
&& said.contains("OPT-todo"),
"{said}"
);
assert!(
board
.status_options()
.iter()
.any(|option| option["name"] == "Queued")
);
assert!(board.priority_options().is_none());
}
#[test]
fn a_persons_own_field_is_read_past_whatever_it_holds() {
let (sandbox, board) = configured();
board.with_persons_field(json!({"__typename": "ProjectV2SingleSelectField",
"id": "FIELD-size", "name": "Size",
"options": [{"id": "OPT-size-s", "name": "S", "color": "TEAL", "description": ""}]}));
board.without_priority_option("Low");
let planned = report(&sandbox, &["--json", "sources", "fields", "board"]);
let names: Vec<&Value> = planned["fields"]
.as_array()
.expect("fields")
.iter()
.map(|field| &field["field"])
.collect();
assert_eq!(names, [&json!("Status"), &json!("Priority")]);
assert_eq!(planned["fields"][1]["missing"], json!(["Low"]));
let status = report(&sandbox, &["--json", "sources", "status-options", "board"]);
assert_eq!(status["missing"], json!([]));
let applied = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
assert_eq!(applied["fields"][1]["outcome"], "applied");
}
fn refused_without_a_write(sandbox: &Sandbox, board: &GitHubBoardFields, refusal: &str) {
for arguments in [
&["sources", "fields", "board"][..],
&["sources", "fields", "board", "--apply"][..],
] {
let output = sandbox
.command()
.args(arguments)
.assert()
.failure()
.get_output()
.clone();
assert!(
stderr(&output).contains(refusal),
"`onetaskgraph {}` did not refuse naming {refusal:?}:\n{}",
arguments.join(" "),
stderr(&output)
);
}
assert!(mutations(board).is_empty(), "{:?}", mutations(board));
}
#[test]
fn a_board_answering_two_status_or_two_priority_fields_is_refused_rather_than_one_chosen() {
for name in ["Status", "Priority"] {
let (sandbox, board) = configured();
board.with_persons_field(json!({"__typename": "ProjectV2SingleSelectField",
"id": "FIELD-size", "name": "Size",
"options": [{"id": "OPT-size-s", "name": "S", "color": "TEAL", "description": ""}]}));
let planned = report(&sandbox, &["--json", "sources", "fields", "board"]);
assert_eq!(planned["fields"].as_array().expect("fields").len(), 2);
board.with_persons_field(json!({"__typename": "ProjectV2SingleSelectField",
"id": format!("FIELD-{name}-again"), "name": name,
"options": [{"id": "OPT-again", "name": "Again", "color": "GRAY",
"description": ""}]}));
refused_without_a_write(
&sandbox,
&board,
&format!("GitHub answered two {name} fields for this board"),
);
}
}
#[test]
fn an_item_answered_with_two_values_of_one_field_is_refused_rather_than_one_kept() {
for name in ["Status", "Priority"] {
let (sandbox, board) = configured();
let applied = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
assert_eq!(applied["fields"][0]["outcome"], "unchanged");
board.repeat_snapshot_value(name);
refused_without_a_write(
&sandbox,
&board,
&format!("GitHub answered two {name} values for board item"),
);
}
}
#[test]
fn a_verification_read_that_fails_after_the_write_is_refused_with_the_recovery_data() {
let (sandbox, board) = configured();
board.without_priority_option("Low");
board.hide_after_update();
let output = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.failure()
.get_output()
.clone();
let said = stderr(&output);
assert!(
said.contains(
"changed the Priority field and then could not read the board back to verify it"
) && said.contains("the pre-write item assignments are:")
&& said.contains("OPT-p-high"),
"{said}"
);
}
#[test]
fn a_priority_field_status_options_does_not_own_cannot_refuse_it() {
let (sandbox, board) = configured();
board.recolor_priority_option("High", "TEAL");
let status = report(&sandbox, &["--json", "sources", "status-options", "board"]);
assert_eq!(status["missing"], json!([]));
assert_eq!(status["outcome"], "planned");
let output = sandbox
.command()
.args(["sources", "fields", "board"])
.assert()
.failure()
.get_output()
.clone();
assert!(
stderr(&output).contains("GitHub single-select option color is invalid"),
"{}",
stderr(&output)
);
}
#[test]
fn a_priority_field_of_another_type_is_refused_rather_than_created_beside() {
let (sandbox, board) = configured();
board.without_priority_field();
board.with_persons_field(json!({"__typename": "ProjectV2Field",
"id": "FIELD-priority-text", "name": "Priority"}));
for arguments in [
&["sources", "fields", "board"][..],
&["sources", "fields", "board", "--apply"][..],
] {
let output = sandbox
.command()
.args(arguments)
.assert()
.failure()
.get_output()
.clone();
assert!(
stderr(&output).contains(
"board's board has a Priority field that is not a single-select field (it is a \
ProjectV2Field)"
) && stderr(&output).contains("next:"),
"{}",
stderr(&output)
);
}
assert!(mutations(&board).is_empty(), "nothing was created");
}
#[test]
fn a_reminted_or_dropped_priority_option_is_refused_with_the_recovery_data() {
for (drift, said) in [
(
GitHubBoardFields::remint_after_priority_update as fn(&GitHubBoardFields),
"a pre-existing Priority option id, name, color or description",
),
(
GitHubBoardFields::omit_added_priority_option,
"an added Priority option",
),
] {
let (sandbox, board) = configured();
board.without_priority_option("Medium");
drift(&board);
let output = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.failure()
.get_output()
.clone();
let stderr = stderr(&output);
assert!(
stderr.contains(&format!("GitHub changed {said}"))
&& stderr.contains("the pre-write item assignments are:")
&& stderr.contains("OPT-p-high"),
"{stderr}"
);
}
}
#[test]
fn drift_after_the_write_is_refused_with_the_pre_write_assignments() {
let (sandbox, board) = configured();
board.without_priority_option("Medium");
board.drift_after_priority_update();
let output = sandbox
.command()
.args(["sources", "fields", "board", "--apply"])
.assert()
.failure()
.get_output()
.clone();
let said = stderr(&output);
assert!(
said.contains("an item's Priority value")
&& said.contains("the pre-write item assignments are:")
&& said.contains("\"Priority\"")
&& said.contains("OPT-p-high"),
"{said}"
);
}
#[test]
fn a_board_without_status_and_a_source_that_is_not_a_board_are_refused_by_name() {
let (sandbox, board) = configured();
board.without_status_field();
let output = sandbox
.command()
.args(["sources", "fields", "board"])
.assert()
.failure()
.get_output()
.clone();
assert!(
stderr(&output).contains("source board board has no Status field"),
"{}",
stderr(&output)
);
let sandbox = Sandbox::new();
sandbox.project_document(&document(&json!({
"notes": {"plugin": "in-memory", "config": {}}
})));
for (source, said) in [
(
"notes",
"source notes uses plugin in-memory, not github-projects; fields is only available for github-projects sources",
),
("absent", "no configured source is named absent"),
] {
let output = sandbox
.command()
.args(["sources", "fields", source])
.assert()
.failure()
.get_output()
.clone();
assert!(stderr(&output).contains(said), "{}", stderr(&output));
}
}
#[test]
fn a_board_with_no_priority_mapping_sets_up_its_status_field_alone() {
let sandbox = Sandbox::new();
let (mut config, board) = github_projects_with_board(&sandbox);
config
.as_object_mut()
.expect("a config block")
.remove("priority_mapping");
sandbox.project_document(&document(&json!({"board": {
"plugin": "github-projects", "config": config
}})));
board.without_priority_field();
let applied = report(
&sandbox,
&["--json", "sources", "fields", "board", "--apply"],
);
let fields = applied["fields"].as_array().expect("fields");
assert_eq!(fields.len(), 1);
assert_eq!(fields[0]["field"], "Status");
assert!(
board.priority_options().is_none(),
"no Priority field is created"
);
assert!(mutations(&board).is_empty());
}