diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 13:48:56 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-07 13:48:56 +0000 |
| commit | 39cfcd950eaf31eb721c25b0e60c751d0f279bb6 (patch) | |
| tree | feb627bc905bcf50ad1925524db32a8fb2b3c58c /src/git | |
| parent | 37c9d3e0d195b0789f9e6407b81973cf50222b76 (diff) | |
purgatory: more robust process_purgatory_state_events syncing
Diffstat (limited to 'src/git')
| -rw-r--r-- | src/git/sync.rs | 425 |
1 files changed, 362 insertions, 63 deletions
diff --git a/src/git/sync.rs b/src/git/sync.rs index cf6e93d..949d8e1 100644 --- a/src/git/sync.rs +++ b/src/git/sync.rs | |||
| @@ -37,8 +37,7 @@ use tracing::{debug, info, warn}; | |||
| 37 | use nostr_sdk::Event; | 37 | use nostr_sdk::Event; |
| 38 | 38 | ||
| 39 | use crate::git::authorization::{ | 39 | use crate::git::authorization::{ |
| 40 | collect_authorized_maintainers, fetch_repository_data, pubkey_authorised_for_repo_owners, | 40 | collect_authorized_maintainers, fetch_repository_data, RepositoryData, |
| 41 | RepositoryData, | ||
| 42 | }; | 41 | }; |
| 43 | use crate::git::{self, oid_exists}; | 42 | use crate::git::{self, oid_exists}; |
| 44 | use crate::nostr::builder::SharedDatabase; | 43 | use crate::nostr::builder::SharedDatabase; |
| @@ -877,25 +876,49 @@ async fn process_purgatory_state_events( | |||
| 877 | let mut result = ProcessResult::default(); | 876 | let mut result = ProcessResult::default(); |
| 878 | 877 | ||
| 879 | // Find state events in purgatory for this identifier | 878 | // Find state events in purgatory for this identifier |
| 880 | let purgatory_states = purgatory.find_state(identifier); | 879 | let mut purgatory_states = purgatory.find_state(identifier); |
| 881 | if purgatory_states.is_empty() { | 880 | if purgatory_states.is_empty() { |
| 882 | return result; | 881 | return result; |
| 883 | } | 882 | } |
| 884 | 883 | ||
| 884 | // Sort by created_at (oldest first) so we process events in chronological order. | ||
| 885 | // This ensures that when multiple state events are in purgatory, older ones | ||
| 886 | // get processed first, allowing newer ones to correctly supersede them. | ||
| 887 | purgatory_states.sort_by_key(|entry| entry.event.created_at); | ||
| 888 | |||
| 885 | debug!( | 889 | debug!( |
| 886 | identifier = %identifier, | 890 | identifier = %identifier, |
| 887 | purgatory_states_count = purgatory_states.len(), | 891 | purgatory_states_count = purgatory_states.len(), |
| 888 | "Checking purgatory state events for available git data" | 892 | "Checking purgatory state events for available git data (processing oldest first)" |
| 889 | ); | 893 | ); |
| 890 | 894 | ||
| 891 | // Check which state events can be applied (have all required OIDs) | 895 | // Fetch repository data once for all state events |
| 896 | let mut db_repo_data = match fetch_repository_data(database, identifier).await { | ||
| 897 | Ok(data) => data, | ||
| 898 | Err(e) => { | ||
| 899 | warn!( | ||
| 900 | identifier = %identifier, | ||
| 901 | error = %e, | ||
| 902 | "Failed to fetch repository data for purgatory state events" | ||
| 903 | ); | ||
| 904 | result | ||
| 905 | .errors | ||
| 906 | .push(format!("Failed to fetch repo data: {}", e)); | ||
| 907 | return result; | ||
| 908 | } | ||
| 909 | }; | ||
| 910 | |||
| 911 | // Collect authorized maintainers per owner (computed once) | ||
| 912 | let by_owner = collect_authorized_maintainers(&db_repo_data.announcements); | ||
| 913 | |||
| 914 | // Process each state event in chronological order | ||
| 892 | for entry in &purgatory_states { | 915 | for entry in &purgatory_states { |
| 893 | // Check if we have all the git data needed to apply this state event | 916 | // Step 0: Check if we have all the git data needed to apply this state event |
| 894 | if !can_apply_state(&entry.event, source_repo_path) { | 917 | if !can_apply_state(&entry.event, source_repo_path) { |
| 895 | debug!( | 918 | debug!( |
| 896 | identifier = %identifier, | 919 | identifier = %identifier, |
| 897 | event_id = %entry.event.id, | 920 | event_id = %entry.event.id, |
| 898 | "State event cannot be applied - missing git OIDs" | 921 | "State event cannot be applied - missing git OIDs in source repo" |
| 899 | ); | 922 | ); |
| 900 | continue; | 923 | continue; |
| 901 | } | 924 | } |
| @@ -910,84 +933,196 @@ async fn process_purgatory_state_events( | |||
| 910 | error = %e, | 933 | error = %e, |
| 911 | "Failed to parse state event from purgatory" | 934 | "Failed to parse state event from purgatory" |
| 912 | ); | 935 | ); |
| 913 | result.errors.push(format!("Failed to parse state event: {}", e)); | 936 | result |
| 937 | .errors | ||
| 938 | .push(format!("Failed to parse state event: {}", e)); | ||
| 914 | continue; | 939 | continue; |
| 915 | } | 940 | } |
| 916 | }; | 941 | }; |
| 917 | 942 | ||
| 918 | // Fetch repository data for authorization check | 943 | let state_author = state.event.pubkey.to_hex(); |
| 919 | let db_repo_data = match fetch_repository_data(database, identifier).await { | 944 | |
| 920 | Ok(data) => data, | 945 | // Step 1: Identify owner repos that the state event author is maintainer for |
| 921 | Err(e) => { | 946 | let authorized_owners: Vec<&String> = by_owner |
| 922 | warn!( | 947 | .iter() |
| 923 | identifier = %identifier, | 948 | .filter(|(_, maintainers)| maintainers.contains(&state_author)) |
| 924 | event_id = %entry.event.id, | 949 | .map(|(owner, _)| owner) |
| 925 | error = %e, | 950 | .collect(); |
| 926 | "Failed to fetch repository data for state event" | ||
| 927 | ); | ||
| 928 | result.errors.push(format!("Failed to fetch repo data: {}", e)); | ||
| 929 | continue; | ||
| 930 | } | ||
| 931 | }; | ||
| 932 | 951 | ||
| 933 | // Check authorization at release time | 952 | if authorized_owners.is_empty() { |
| 934 | let repo_owners_authorising_pubkey = | ||
| 935 | pubkey_authorised_for_repo_owners(&entry.event.pubkey, &db_repo_data); | ||
| 936 | if repo_owners_authorising_pubkey.is_empty() { | ||
| 937 | debug!( | 953 | debug!( |
| 938 | identifier = %identifier, | 954 | identifier = %identifier, |
| 939 | event_id = %entry.event.id, | 955 | event_id = %entry.event.id, |
| 940 | pubkey = %entry.event.pubkey, | 956 | pubkey = %state_author, |
| 941 | "State event author no longer authorized - skipping" | 957 | "State event author not authorized for any owner - skipping" |
| 942 | ); | 958 | ); |
| 943 | continue; | 959 | continue; |
| 944 | } | 960 | } |
| 945 | 961 | ||
| 946 | // Sync to owner repos and align refs | 962 | // Track if we applied to at least one owner repo |
| 947 | let sync_result = sync_to_owner_repos(source_repo_path, &state, &db_repo_data, git_data_path); | 963 | let mut applied_to_any = false; |
| 948 | result.repos_synced += sync_result.repos_synced; | ||
| 949 | result.refs_created += sync_result.refs_created; | ||
| 950 | result.refs_updated += sync_result.refs_updated; | ||
| 951 | result.refs_deleted += sync_result.refs_deleted; | ||
| 952 | 964 | ||
| 953 | // Save event to database | 965 | // Process each owner repo that authorizes this state event author |
| 954 | match database.save_event(&entry.event).await { | 966 | for owner in &authorized_owners { |
| 955 | Ok(_) => { | 967 | let maintainers = by_owner.get(*owner).unwrap(); |
| 956 | info!( | 968 | |
| 969 | // Step 2: Check if this state event is the latest authorized for this owner | ||
| 970 | // Only consider database states, not other purgatory states | ||
| 971 | let is_latest = is_latest_authorized_state( | ||
| 972 | &state, | ||
| 973 | maintainers, | ||
| 974 | &db_repo_data.states, | ||
| 975 | ); | ||
| 976 | |||
| 977 | if !is_latest { | ||
| 978 | debug!( | ||
| 957 | identifier = %identifier, | 979 | identifier = %identifier, |
| 958 | event_id = %entry.event.id, | 980 | event_id = %entry.event.id, |
| 959 | "Saved purgatory state event to database" | 981 | owner = %owner, |
| 982 | "Skipping owner - a newer authorized state exists" | ||
| 960 | ); | 983 | ); |
| 984 | continue; | ||
| 985 | } | ||
| 961 | 986 | ||
| 962 | // Notify WebSocket subscribers | 987 | // Find the announcement for this owner |
| 963 | if let Some(relay) = local_relay { | 988 | let announcement = db_repo_data |
| 964 | if relay.notify_event(entry.event.clone()) { | 989 | .announcements |
| 965 | debug!( | 990 | .iter() |
| 966 | identifier = %identifier, | 991 | .find(|a| a.event.pubkey.to_hex() == **owner); |
| 967 | event_id = %entry.event.id, | ||
| 968 | "Broadcast state event to WebSocket listeners" | ||
| 969 | ); | ||
| 970 | } | ||
| 971 | } | ||
| 972 | 992 | ||
| 973 | // Remove from purgatory | 993 | let Some(announcement) = announcement else { |
| 974 | purgatory.remove_state_event(identifier, &entry.event.id); | 994 | continue; |
| 975 | result.states_released += 1; | 995 | }; |
| 976 | 996 | ||
| 977 | info!( | 997 | let target_repo_path = git_data_path.join(announcement.repo_path()); |
| 998 | |||
| 999 | // Step 3: Check git repo exists for that owner | ||
| 1000 | if !target_repo_path.exists() { | ||
| 1001 | debug!( | ||
| 978 | identifier = %identifier, | 1002 | identifier = %identifier, |
| 979 | event_id = %entry.event.id, | 1003 | owner = %owner, |
| 980 | "Released state event from purgatory" | 1004 | repo_path = %target_repo_path.display(), |
| 1005 | "Skipping owner - repository doesn't exist" | ||
| 981 | ); | 1006 | ); |
| 1007 | continue; | ||
| 982 | } | 1008 | } |
| 983 | Err(e) => { | 1009 | |
| 984 | warn!( | 1010 | // Step 4: Copy all required OIDs to that repo (unless it's source_repo_path) |
| 985 | identifier = %identifier, | 1011 | if target_repo_path != source_repo_path { |
| 986 | event_id = %entry.event.id, | 1012 | if let Err(e) = |
| 987 | error = %e, | 1013 | copy_missing_oids_between_repos(source_repo_path, &target_repo_path, &state) |
| 988 | "Failed to save state event to database" | 1014 | { |
| 989 | ); | 1015 | warn!( |
| 990 | result.errors.push(format!("Failed to save state event: {}", e)); | 1016 | identifier = %identifier, |
| 1017 | source = %source_repo_path.display(), | ||
| 1018 | target = %target_repo_path.display(), | ||
| 1019 | error = %e, | ||
| 1020 | "Failed to copy OIDs between repos" | ||
| 1021 | ); | ||
| 1022 | result | ||
| 1023 | .errors | ||
| 1024 | .push((target_repo_path.display().to_string(), e).1); | ||
| 1025 | // Continue anyway - we'll try to align what we can | ||
| 1026 | } | ||
| 1027 | } | ||
| 1028 | |||
| 1029 | // Step 5: Reset the git state in that repo to match the state event | ||
| 1030 | // (excluding refs/nostr/*) | ||
| 1031 | let align_result = align_repository_with_state(&target_repo_path, &state); | ||
| 1032 | result.repos_synced += 1; | ||
| 1033 | result.refs_created += align_result.refs_created; | ||
| 1034 | result.refs_updated += align_result.refs_updated; | ||
| 1035 | result.refs_deleted += align_result.refs_deleted; | ||
| 1036 | |||
| 1037 | info!( | ||
| 1038 | identifier = %identifier, | ||
| 1039 | owner = %owner, | ||
| 1040 | event_id = %entry.event.id, | ||
| 1041 | repo_path = %target_repo_path.display(), | ||
| 1042 | refs_created = align_result.refs_created, | ||
| 1043 | refs_updated = align_result.refs_updated, | ||
| 1044 | refs_deleted = align_result.refs_deleted, | ||
| 1045 | head_set = align_result.head_set, | ||
| 1046 | "Aligned repository with state from purgatory" | ||
| 1047 | ); | ||
| 1048 | |||
| 1049 | applied_to_any = true; | ||
| 1050 | } | ||
| 1051 | |||
| 1052 | // We have the git data now, so we should release from purgatory regardless of | ||
| 1053 | // whether we applied to any repo. The question is: should we save to DB or just remove? | ||
| 1054 | // | ||
| 1055 | // - If there's a newer state event from the same author already in the DB, just remove | ||
| 1056 | // (no point saving an older event that will never be used) | ||
| 1057 | // - Otherwise, save it to the DB (even if we didn't apply to any repo, because in the | ||
| 1058 | // future the currently-authorized state event might be deleted and this one should apply) | ||
| 1059 | |||
| 1060 | // Check if there's a newer state from the same author in the database | ||
| 1061 | let has_newer_from_same_author = db_repo_data.states.iter().any(|s| { | ||
| 1062 | s.event.pubkey == state.event.pubkey | ||
| 1063 | && (s.event.created_at > state.event.created_at | ||
| 1064 | || (s.event.created_at == state.event.created_at | ||
| 1065 | && s.event.id > state.event.id)) | ||
| 1066 | }); | ||
| 1067 | |||
| 1068 | if has_newer_from_same_author { | ||
| 1069 | // Just remove from purgatory without saving - a newer event from same author exists | ||
| 1070 | purgatory.remove_state_event(identifier, &entry.event.id); | ||
| 1071 | result.states_released += 1; | ||
| 1072 | |||
| 1073 | debug!( | ||
| 1074 | identifier = %identifier, | ||
| 1075 | event_id = %entry.event.id, | ||
| 1076 | author = %state_author, | ||
| 1077 | "Removed older state event from purgatory - newer event from same author exists in DB" | ||
| 1078 | ); | ||
| 1079 | } else { | ||
| 1080 | // Save to database (even if we didn't apply to any repo) | ||
| 1081 | match database.save_event(&entry.event).await { | ||
| 1082 | Ok(_) => { | ||
| 1083 | info!( | ||
| 1084 | identifier = %identifier, | ||
| 1085 | event_id = %entry.event.id, | ||
| 1086 | applied_to_repos = applied_to_any, | ||
| 1087 | "Saved purgatory state event to database" | ||
| 1088 | ); | ||
| 1089 | |||
| 1090 | // Notify WebSocket subscribers | ||
| 1091 | if let Some(relay) = local_relay { | ||
| 1092 | if relay.notify_event(entry.event.clone()) { | ||
| 1093 | debug!( | ||
| 1094 | identifier = %identifier, | ||
| 1095 | event_id = %entry.event.id, | ||
| 1096 | "Broadcast state event to WebSocket listeners" | ||
| 1097 | ); | ||
| 1098 | } | ||
| 1099 | } | ||
| 1100 | |||
| 1101 | // Remove from purgatory | ||
| 1102 | purgatory.remove_state_event(identifier, &entry.event.id); | ||
| 1103 | result.states_released += 1; | ||
| 1104 | |||
| 1105 | // Add the newly saved state to db_repo_data so subsequent iterations | ||
| 1106 | // can correctly determine if they're the latest | ||
| 1107 | db_repo_data.states.push(state.clone()); | ||
| 1108 | |||
| 1109 | info!( | ||
| 1110 | identifier = %identifier, | ||
| 1111 | event_id = %entry.event.id, | ||
| 1112 | "Released state event from purgatory" | ||
| 1113 | ); | ||
| 1114 | } | ||
| 1115 | Err(e) => { | ||
| 1116 | warn!( | ||
| 1117 | identifier = %identifier, | ||
| 1118 | event_id = %entry.event.id, | ||
| 1119 | error = %e, | ||
| 1120 | "Failed to save state event to database" | ||
| 1121 | ); | ||
| 1122 | result | ||
| 1123 | .errors | ||
| 1124 | .push(format!("Failed to save state event: {}", e)); | ||
| 1125 | } | ||
| 991 | } | 1126 | } |
| 992 | } | 1127 | } |
| 993 | } | 1128 | } |
| @@ -995,6 +1130,45 @@ async fn process_purgatory_state_events( | |||
| 995 | result | 1130 | result |
| 996 | } | 1131 | } |
| 997 | 1132 | ||
| 1133 | /// Check if a state event is the latest authorized state for a given maintainer set. | ||
| 1134 | /// | ||
| 1135 | /// Only considers states already in the database, not other purgatory states. | ||
| 1136 | /// | ||
| 1137 | /// # Arguments | ||
| 1138 | /// * `state` - The state event to check | ||
| 1139 | /// * `maintainers` - The set of authorized maintainers for the owner | ||
| 1140 | /// * `db_states` - State events from the database | ||
| 1141 | /// | ||
| 1142 | /// # Returns | ||
| 1143 | /// true if this state is the latest (or equal latest) among all authorized states in the DB | ||
| 1144 | fn is_latest_authorized_state( | ||
| 1145 | state: &RepositoryState, | ||
| 1146 | maintainers: &[String], | ||
| 1147 | db_states: &[RepositoryState], | ||
| 1148 | ) -> bool { | ||
| 1149 | // Find the latest authorized state from database | ||
| 1150 | let latest_db_state = db_states | ||
| 1151 | .iter() | ||
| 1152 | .filter(|s| maintainers.contains(&s.event.pubkey.to_hex())) | ||
| 1153 | .max_by(|a, b| { | ||
| 1154 | // Compare by created_at, then by event id for tie-breaking | ||
| 1155 | a.event | ||
| 1156 | .created_at | ||
| 1157 | .cmp(&b.event.created_at) | ||
| 1158 | .then_with(|| a.event.id.cmp(&b.event.id)) | ||
| 1159 | }); | ||
| 1160 | |||
| 1161 | match latest_db_state { | ||
| 1162 | None => true, // No other states exist in DB, this is the latest | ||
| 1163 | Some(latest) => { | ||
| 1164 | // This state is latest if it's newer, or if equal timestamp with larger event id | ||
| 1165 | state.event.created_at > latest.event.created_at | ||
| 1166 | || (state.event.created_at == latest.event.created_at | ||
| 1167 | && state.event.id >= latest.event.id) | ||
| 1168 | } | ||
| 1169 | } | ||
| 1170 | } | ||
| 1171 | |||
| 998 | /// Process PR events from purgatory that can now be satisfied. | 1172 | /// Process PR events from purgatory that can now be satisfied. |
| 999 | async fn process_purgatory_pr_events( | 1173 | async fn process_purgatory_pr_events( |
| 1000 | identifier: &str, | 1174 | identifier: &str, |
| @@ -1146,6 +1320,7 @@ fn extract_owner_from_repo_path(repo_path: &Path, git_data_path: &Path) -> Optio | |||
| 1146 | #[cfg(test)] | 1320 | #[cfg(test)] |
| 1147 | mod tests { | 1321 | mod tests { |
| 1148 | use super::*; | 1322 | use super::*; |
| 1323 | use nostr_sdk::Keys; | ||
| 1149 | 1324 | ||
| 1150 | #[test] | 1325 | #[test] |
| 1151 | fn test_process_result_default() { | 1326 | fn test_process_result_default() { |
| @@ -1427,4 +1602,128 @@ mod tests { | |||
| 1427 | assert_eq!(owners.len(), 1); | 1602 | assert_eq!(owners.len(), 1); |
| 1428 | assert!(owners.contains("valid_owner")); | 1603 | assert!(owners.contains("valid_owner")); |
| 1429 | } | 1604 | } |
| 1605 | |||
| 1606 | // Helper function to create a test state event with specific timestamp | ||
| 1607 | // The `nonce` parameter ensures different events have different IDs even with same timestamp | ||
| 1608 | fn create_test_state_event_with_nonce( | ||
| 1609 | keys: &Keys, | ||
| 1610 | identifier: &str, | ||
| 1611 | created_at: u64, | ||
| 1612 | nonce: &str, | ||
| 1613 | ) -> RepositoryState { | ||
| 1614 | use nostr_sdk::{EventBuilder, Kind, Tag, TagKind, Timestamp}; | ||
| 1615 | |||
| 1616 | let tags = vec![ | ||
| 1617 | Tag::custom(TagKind::d(), vec![identifier.to_string()]), | ||
| 1618 | Tag::custom( | ||
| 1619 | TagKind::custom("refs/heads/main"), | ||
| 1620 | vec![format!("abc123{}", nonce)], | ||
| 1621 | ), | ||
| 1622 | ]; | ||
| 1623 | |||
| 1624 | let event = EventBuilder::new(Kind::from(30618), nonce) | ||
| 1625 | .tags(tags) | ||
| 1626 | .custom_created_at(Timestamp::from(created_at)) | ||
| 1627 | .sign_with_keys(keys) | ||
| 1628 | .unwrap(); | ||
| 1629 | |||
| 1630 | RepositoryState::from_event(event).unwrap() | ||
| 1631 | } | ||
| 1632 | |||
| 1633 | // Helper function to create a test state event with specific timestamp | ||
| 1634 | fn create_test_state_event( | ||
| 1635 | keys: &Keys, | ||
| 1636 | identifier: &str, | ||
| 1637 | created_at: u64, | ||
| 1638 | ) -> RepositoryState { | ||
| 1639 | create_test_state_event_with_nonce(keys, identifier, created_at, "") | ||
| 1640 | } | ||
| 1641 | |||
| 1642 | #[test] | ||
| 1643 | fn test_is_latest_authorized_state_no_other_states() { | ||
| 1644 | let keys = Keys::generate(); | ||
| 1645 | let state = create_test_state_event(&keys, "test-repo", 1000); | ||
| 1646 | let maintainers = vec![keys.public_key().to_hex()]; | ||
| 1647 | |||
| 1648 | // No other states - should be latest | ||
| 1649 | let result = is_latest_authorized_state(&state, &maintainers, &[]); | ||
| 1650 | assert!(result); | ||
| 1651 | } | ||
| 1652 | |||
| 1653 | #[test] | ||
| 1654 | fn test_is_latest_authorized_state_newer_than_db() { | ||
| 1655 | let keys = Keys::generate(); | ||
| 1656 | let old_state = create_test_state_event(&keys, "test-repo", 1000); | ||
| 1657 | let new_state = create_test_state_event(&keys, "test-repo", 2000); | ||
| 1658 | let maintainers = vec![keys.public_key().to_hex()]; | ||
| 1659 | |||
| 1660 | // new_state is newer than old_state in db | ||
| 1661 | let result = is_latest_authorized_state(&new_state, &maintainers, &[old_state]); | ||
| 1662 | assert!(result); | ||
| 1663 | } | ||
| 1664 | |||
| 1665 | #[test] | ||
| 1666 | fn test_is_latest_authorized_state_older_than_db() { | ||
| 1667 | let keys = Keys::generate(); | ||
| 1668 | let old_state = create_test_state_event(&keys, "test-repo", 1000); | ||
| 1669 | let new_state = create_test_state_event(&keys, "test-repo", 2000); | ||
| 1670 | let maintainers = vec![keys.public_key().to_hex()]; | ||
| 1671 | |||
| 1672 | // old_state is older than new_state in db | ||
| 1673 | let result = is_latest_authorized_state(&old_state, &maintainers, &[new_state]); | ||
| 1674 | assert!(!result); | ||
| 1675 | } | ||
| 1676 | |||
| 1677 | #[test] | ||
| 1678 | fn test_is_latest_authorized_state_ignores_unauthorized_states() { | ||
| 1679 | let keys1 = Keys::generate(); | ||
| 1680 | let keys2 = Keys::generate(); | ||
| 1681 | |||
| 1682 | let state1 = create_test_state_event(&keys1, "test-repo", 1000); | ||
| 1683 | let state2 = create_test_state_event(&keys2, "test-repo", 2000); | ||
| 1684 | |||
| 1685 | // Only keys1 is authorized | ||
| 1686 | let maintainers = vec![keys1.public_key().to_hex()]; | ||
| 1687 | |||
| 1688 | // state1 should be latest because state2 is not authorized | ||
| 1689 | let result = is_latest_authorized_state(&state1, &maintainers, &[state2]); | ||
| 1690 | assert!(result); | ||
| 1691 | } | ||
| 1692 | |||
| 1693 | #[test] | ||
| 1694 | fn test_is_latest_authorized_state_same_timestamp_uses_event_id() { | ||
| 1695 | let keys = Keys::generate(); | ||
| 1696 | |||
| 1697 | // Create two states with same timestamp but different content (different event IDs) | ||
| 1698 | let state1 = create_test_state_event_with_nonce(&keys, "test-repo", 1000, "nonce1"); | ||
| 1699 | let state2 = create_test_state_event_with_nonce(&keys, "test-repo", 1000, "nonce2"); | ||
| 1700 | |||
| 1701 | let maintainers = vec![keys.public_key().to_hex()]; | ||
| 1702 | |||
| 1703 | // The one with larger event ID should be considered latest | ||
| 1704 | let (latest, older) = if state1.event.id > state2.event.id { | ||
| 1705 | (state1, state2) | ||
| 1706 | } else { | ||
| 1707 | (state2, state1) | ||
| 1708 | }; | ||
| 1709 | |||
| 1710 | // latest should be considered latest | ||
| 1711 | let result = is_latest_authorized_state(&latest, &maintainers, &[older.clone()]); | ||
| 1712 | assert!(result); | ||
| 1713 | |||
| 1714 | // older should not be considered latest | ||
| 1715 | let result = is_latest_authorized_state(&older, &maintainers, &[latest]); | ||
| 1716 | assert!(!result); | ||
| 1717 | } | ||
| 1718 | |||
| 1719 | #[test] | ||
| 1720 | fn test_is_latest_authorized_state_same_event_is_latest() { | ||
| 1721 | let keys = Keys::generate(); | ||
| 1722 | let state = create_test_state_event(&keys, "test-repo", 1000); | ||
| 1723 | let maintainers = vec![keys.public_key().to_hex()]; | ||
| 1724 | |||
| 1725 | // When the state being checked is also in the db_states, it should be considered latest | ||
| 1726 | let result = is_latest_authorized_state(&state, &maintainers, &[state.clone()]); | ||
| 1727 | assert!(result); | ||
| 1728 | } | ||
| 1430 | } | 1729 | } |