| ... | ... | @@ -288,16 +288,21 @@ fn issue(html: &str, repository: &str, id: Option<u64>) -> Result<Value> { |
| 288 | 288 | let spans: Vec<_> = document |
| 289 | 289 | .select(&Selector::parse("h1 > span").unwrap()) |
| 290 | 290 | .collect(); |
| 291 | | let issue_id = spans.first().and_then(|s| { |
| 292 | | text(*s) |
| 293 | | .strip_prefix('#') |
| 294 | | .and_then(|s| s.parse::<u64>().ok()) |
| 295 | | }); |
| 291 | let (issue_id, title) = if spans.len() == 2 { |
| 292 | (text(spans[0]).strip_prefix('#').and_then(|id| id.parse::<u64>().ok()), Some(text(spans[1]))) |
| 293 | } else if spans.is_empty() && tokenless_r1616(&document) { |
| 294 | let headings: Vec<_> = document.select(&Selector::parse("h1").unwrap()).collect(); |
| 295 | if let [heading] = headings.as_slice() { |
| 296 | text(*heading).strip_prefix("Issue #").and_then(|text| text.split_once(": ")) |
| 297 | .map(|(id, title)| (id.parse::<u64>().ok(), Some(title.to_owned()))) |
| 298 | .unwrap_or((None, None)) |
| 299 | } else { (None, None) } |
| 300 | } else { (None, None) }; |
| 296 | 301 | let status = document |
| 297 | 302 | .select(&Selector::parse("select[name=status] option[selected]").unwrap()) |
| 298 | 303 | .next() |
| 299 | 304 | .and_then(|e| e.attr("value")); |
| 300 | | if spans.len() != 2 |
| 305 | if title.as_ref().is_none_or(|title| title.is_empty()) |
| 301 | 306 | || issue_id.is_none_or(|n| n == 0 || id.is_some_and(|id| n != id)) |
| 302 | 307 | || status.is_none() |
| 303 | 308 | { |
| ... | ... | @@ -334,7 +339,7 @@ fn issue(html: &str, repository: &str, id: Option<u64>) -> Result<Value> { |
| 334 | 339 | .map(text) |
| 335 | 340 | .collect(); |
| 336 | 341 | Ok( |
| 337 | | json!({"repository":repository,"id":issue_id,"title":text(spans[1]),"status":status,"labels":labels,"comments":comments}), |
| 342 | json!({"repository":repository,"id":issue_id,"title":title.unwrap(),"status":status,"labels":labels,"comments":comments}), |
| 338 | 343 | ) |
| 339 | 344 | } |
| 340 | 345 | fn current(app: &App, grant: &Value, credential: &str) -> Result<()> { |
| ... | ... | @@ -921,6 +926,15 @@ mod tests { |
| 921 | 926 | assert!(issue(&page.replace("selected", ""), "owned", Some(3)).is_err()); |
| 922 | 927 | } |
| 923 | 928 | #[test] |
| 929 | fn verified_r1616_issue_heading_keeps_identity_checks() { |
| 930 | let page = "<head><meta name='astheno.shale.repo.name' content='owned'></head><body id=page-issue><h1>Issue #3: Snow &amp; ☃</h1><select name=status><option selected value=done>Done</option></select><footer class=usa-footer><div class=usa-footer__secondary-section><a href='https://astheno.software/shale/'>shale r1616-ga87d2f5.zig.0.16.0</a></div></footer></body>"; |
| 931 | assert_eq!(issue(page, "owned", Some(3)).unwrap()["title"], "Snow & ☃"); |
| 932 | assert!(issue(page, "other", Some(3)).is_err()); |
| 933 | assert!(issue(page, "owned", Some(4)).is_err()); |
| 934 | assert!(issue(&page.replace("r1616-ga87d2f5.zig.0.16.0", "r1758-new"), "owned", Some(3)).is_err()); |
| 935 | assert!(issue(&page.replace("Issue #3", "Issue #0"), "owned", None).is_err()); |
| 936 | } |
| 937 | #[test] |
| 924 | 938 | fn account_identity_uses_html_text_and_rejects_login_or_changed_markup() { |
| 925 | 939 | assert_eq!( |
| 926 | 940 | username("<body id=page-user-settings><kbd>snow&amp;flake☃</kbd>").unwrap(), |