1use crate::{
4 ArtifactHash, Dependencies, Description, IndexEntry, PluginName, PublishedAt, TriggerType,
5};
6
7#[derive(Debug, Clone, Default)]
9pub struct IndexSearchQuery {
10 pub query: Option<String>,
11 pub trigger_type: Option<TriggerType>,
12 pub database_version: Option<semver::Version>,
13 pub include_yanked: bool,
14 pub include_incompatible: bool,
15}
16
17#[derive(Debug, Clone, PartialEq, serde::Serialize)]
19pub struct IndexSearchResult {
20 pub hits: Vec<IndexSearchHit>,
21}
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize)]
25pub struct IndexSearchHit {
26 pub name: PluginName,
27 pub version: semver::Version,
28 pub published_at: PublishedAt,
29 pub description: Description,
30 pub triggers: Vec<TriggerType>,
31 pub visibility: IndexVersionVisibility,
32}
33
34#[derive(Debug, Clone, PartialEq, serde::Serialize)]
36pub enum IndexVersionVisibility {
37 Visible,
38 Hidden { reasons: Vec<IndexVisibilityReason> },
39}
40
41#[derive(Debug, Clone, PartialEq, serde::Serialize)]
43pub enum IndexVisibilityReason {
44 Yanked,
45 IncompatibleDatabaseVersion {
46 required: semver::VersionReq,
47 actual: semver::Version,
48 },
49}
50
51#[derive(Debug, Clone)]
53pub struct IndexInfoQuery {
54 pub name: PluginName,
55 pub version: Option<semver::Version>,
56 pub database_version: Option<semver::Version>,
57 pub include_yanked: bool,
58 pub include_incompatible: bool,
59}
60
61#[derive(Debug, Clone, PartialEq, serde::Serialize)]
63pub enum IndexInfoResult {
64 Found(Box<IndexInfo>),
65 NotFound {
66 name: PluginName,
67 version: Option<semver::Version>,
68 },
69 FilteredOut {
70 name: PluginName,
71 version: Option<semver::Version>,
72 reasons: Vec<IndexVisibilityReason>,
73 },
74}
75
76#[derive(Debug, Clone, PartialEq, serde::Serialize)]
78pub struct IndexInfo {
79 pub name: PluginName,
80 pub version: semver::Version,
81 pub published_at: PublishedAt,
82 pub description: Description,
83 pub triggers: Vec<TriggerType>,
84 pub homepage: Option<url::Url>,
85 pub repository: Option<url::Url>,
86 pub documentation: Option<url::Url>,
87 pub dependencies: Dependencies,
88 pub hash: ArtifactHash,
89 pub visibility: IndexVersionVisibility,
90}
91
92pub(crate) fn visibility_for(
93 entry: &IndexEntry,
94 database_version: Option<&semver::Version>,
95) -> IndexVersionVisibility {
96 let mut reasons = Vec::new();
97 if entry.yanked {
98 reasons.push(IndexVisibilityReason::Yanked);
99 }
100 if let Some(db_ver) =
101 database_version.filter(|v| !entry.dependencies.database_version.matches(v))
102 {
103 reasons.push(IndexVisibilityReason::IncompatibleDatabaseVersion {
104 required: entry.dependencies.database_version.clone(),
105 actual: db_ver.clone(),
106 });
107 }
108 if reasons.is_empty() {
109 IndexVersionVisibility::Visible
110 } else {
111 IndexVersionVisibility::Hidden { reasons }
112 }
113}
114
115fn info_from_entry(entry: &IndexEntry, visibility: IndexVersionVisibility) -> IndexInfo {
116 IndexInfo {
117 name: entry.name.clone(),
118 version: entry.version.clone(),
119 published_at: entry.published_at.clone(),
120 description: entry.description.clone(),
121 triggers: entry.triggers.clone(),
122 homepage: entry.homepage.clone(),
123 repository: entry.repository.clone(),
124 documentation: entry.documentation.clone(),
125 dependencies: entry.dependencies.clone(),
126 hash: entry.hash.clone(),
127 visibility,
128 }
129}
130
131impl crate::Index {
132 pub fn search(&self, query: &IndexSearchQuery) -> IndexSearchResult {
133 use std::collections::BTreeMap;
134
135 let query_text = query
136 .query
137 .as_deref()
138 .map(|s| s.trim())
139 .filter(|s| !s.is_empty());
140 let query_lower = query_text.map(|s| s.to_ascii_lowercase());
141 let query_canonical = query_text.map(crate::identity::canonical_name);
142
143 let mut groups: BTreeMap<String, Vec<(&IndexEntry, IndexVersionVisibility)>> =
145 BTreeMap::new();
146
147 for entry in &self.plugins {
148 let vis = visibility_for(entry, query.database_version.as_ref());
149
150 let excluded = matches!(&vis, IndexVersionVisibility::Hidden { reasons }
151 if reasons.iter().any(|r| match r {
152 IndexVisibilityReason::Yanked => !query.include_yanked,
153 IndexVisibilityReason::IncompatibleDatabaseVersion { .. } => {
154 !query.include_incompatible
155 }
156 })
157 );
158 if excluded {
159 continue;
160 }
161
162 if let Some(ref q_lower) = query_lower {
163 let name_lower = entry.name.as_str().to_ascii_lowercase();
164 let canonical = entry.name.canonical();
165 let desc_lower = entry.description.as_str().to_ascii_lowercase();
166 let q_canon = query_canonical.as_deref().unwrap_or("");
167
168 let matches = name_lower.contains(q_lower.as_str())
169 || canonical.contains(q_canon)
170 || desc_lower.contains(q_lower.as_str());
171 if !matches {
172 continue;
173 }
174 }
175
176 if matches!(&query.trigger_type, Some(t) if !entry.triggers.contains(t)) {
177 continue;
178 }
179
180 groups
181 .entry(entry.name.canonical())
182 .or_default()
183 .push((entry, vis));
184 }
185
186 let mut hits = Vec::with_capacity(groups.len());
187 for (_canonical, mut candidates) in groups {
188 candidates.sort_by(|a, b| {
190 b.0.version
191 .cmp_precedence(&a.0.version)
192 .then_with(|| b.0.version.cmp(&a.0.version))
193 });
194 let (entry, vis) = &candidates[0];
195 hits.push(IndexSearchHit {
196 name: entry.name.clone(),
197 version: entry.version.clone(),
198 published_at: entry.published_at.clone(),
199 description: entry.description.clone(),
200 triggers: entry.triggers.clone(),
201 visibility: vis.clone(),
202 });
203 }
204
205 IndexSearchResult { hits }
206 }
207
208 pub fn info(&self, query: &IndexInfoQuery) -> IndexInfoResult {
209 let query_canonical = query.name.canonical();
210
211 if let Some(ref requested_version) = query.version {
213 let found = self
214 .plugins
215 .iter()
216 .find(|e| e.name.canonical() == query_canonical && e.version == *requested_version);
217 return match found {
218 Some(entry) => {
219 let vis = visibility_for(entry, query.database_version.as_ref());
220 IndexInfoResult::Found(Box::new(info_from_entry(entry, vis)))
221 }
222 None => IndexInfoResult::NotFound {
223 name: query.name.clone(),
224 version: Some(requested_version.clone()),
225 },
226 };
227 }
228
229 let candidates: Vec<(&IndexEntry, IndexVersionVisibility)> = self
231 .plugins
232 .iter()
233 .filter(|e| e.name.canonical() == query_canonical)
234 .map(|e| {
235 let vis = visibility_for(e, query.database_version.as_ref());
236 (e, vis)
237 })
238 .collect();
239
240 if candidates.is_empty() {
241 return IndexInfoResult::NotFound {
242 name: query.name.clone(),
243 version: None,
244 };
245 }
246
247 let (mut selectable, excluded): (Vec<_>, Vec<_>) =
249 candidates.into_iter().partition(|(_, vis)| match vis {
250 IndexVersionVisibility::Visible => true,
251 IndexVersionVisibility::Hidden { reasons } => reasons.iter().all(|r| match r {
252 IndexVisibilityReason::Yanked => query.include_yanked,
253 IndexVisibilityReason::IncompatibleDatabaseVersion { .. } => {
254 query.include_incompatible
255 }
256 }),
257 });
258
259 if selectable.is_empty() {
260 let mut has_yanked = false;
262 let mut incompat_reason: Option<IndexVisibilityReason> = None;
263 for (_, vis) in &excluded {
264 if let IndexVersionVisibility::Hidden { reasons } = vis {
265 for r in reasons {
266 match r {
267 IndexVisibilityReason::Yanked => has_yanked = true,
268 IndexVisibilityReason::IncompatibleDatabaseVersion { .. } => {
269 if incompat_reason.is_none() {
270 incompat_reason = Some(r.clone());
271 }
272 }
273 }
274 }
275 }
276 }
277 let mut reasons = Vec::new();
278 if has_yanked {
279 reasons.push(IndexVisibilityReason::Yanked);
280 }
281 if let Some(ir) = incompat_reason {
282 reasons.push(ir);
283 }
284 return IndexInfoResult::FilteredOut {
285 name: query.name.clone(),
286 version: None,
287 reasons,
288 };
289 }
290
291 selectable.sort_by(|a, b| {
293 b.0.version
294 .cmp_precedence(&a.0.version)
295 .then_with(|| b.0.version.cmp(&a.0.version))
296 });
297 let (entry, vis) = &selectable[0];
298 IndexInfoResult::Found(Box::new(info_from_entry(entry, vis.clone())))
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::{
306 ArtifactHash, ArtifactsUrl, Dependencies, Description, Index, IndexEntry,
307 IndexSchemaVersion, PublishedAt, PythonRequirement, TriggerType,
308 };
309 use assert_matches::assert_matches;
310 use pretty_assertions::assert_eq;
311
312 fn make_entry(name: &str, version: &str) -> IndexEntry {
313 make_entry_with_published_at(name, version, "2026-04-29T18:45:12Z")
314 }
315
316 fn make_entry_with_published_at(name: &str, version: &str, published_at: &str) -> IndexEntry {
317 IndexEntry {
318 name: name.parse().unwrap(),
319 version: version.parse().unwrap(),
320 published_at: PublishedAt::try_new(published_at).unwrap(),
321 description: Description::try_new("desc").unwrap(),
322 triggers: vec![TriggerType::ProcessWrites],
323 homepage: None,
324 repository: None,
325 documentation: None,
326 dependencies: Dependencies {
327 database_version: ">=3.0.0".parse().unwrap(),
328 python: vec![],
329 plugins: vec![],
330 },
331 hash: ArtifactHash::try_new(
332 "sha256:0000000000000000000000000000000000000000000000000000000000000000",
333 )
334 .unwrap(),
335 yanked: false,
336 }
337 }
338
339 fn make_index(entries: Vec<IndexEntry>) -> Index {
340 Index {
341 index_schema_version: IndexSchemaVersion::CURRENT,
342 artifacts_url: ArtifactsUrl::try_new("https://example.com/artifacts").unwrap(),
343 plugins: entries,
344 }
345 }
346
347 #[test]
350 fn visibility_visible_when_not_yanked_and_compatible() {
351 let entry = make_entry("foo", "1.0.0");
352 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
353 assert_eq!(vis, IndexVersionVisibility::Visible);
354 }
355
356 #[test]
357 fn visibility_hidden_when_yanked() {
358 let mut entry = make_entry("foo", "1.0.0");
359 entry.yanked = true;
360 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
361 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
362 assert_eq!(reasons.len(), 1);
363 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
364 });
365 }
366
367 #[test]
368 fn visibility_hidden_when_incompatible() {
369 let mut entry = make_entry("foo", "1.0.0");
370 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
371 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
372 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
373 assert_eq!(reasons.len(), 1);
374 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
375 required, actual
376 } => {
377 assert_eq!(required.to_string(), ">=4.0.0");
378 assert_eq!(*actual, "3.2.0".parse::<semver::Version>().unwrap());
379 });
380 });
381 }
382
383 #[test]
384 fn visibility_hidden_yanked_and_incompatible() {
385 let mut entry = make_entry("foo", "1.0.0");
386 entry.yanked = true;
387 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
388 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
389 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
390 assert_eq!(reasons.len(), 2);
391 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
392 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
393 });
394 }
395
396 #[test]
397 fn visibility_no_compat_check_without_db_version() {
398 let mut entry = make_entry("foo", "1.0.0");
399 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
400 let vis = visibility_for(&entry, None);
401 assert_eq!(vis, IndexVersionVisibility::Visible);
402 }
403
404 #[test]
407 fn search_empty_query_matches_all() {
408 let index = make_index(vec![
409 make_entry("alpha", "1.0.0"),
410 make_entry("beta", "2.0.0"),
411 ]);
412 let result = index.search(&IndexSearchQuery::default());
413 assert_eq!(result.hits.len(), 2);
414 }
415
416 #[test]
417 fn search_whitespace_query_matches_all() {
418 let index = make_index(vec![
419 make_entry("alpha", "1.0.0"),
420 make_entry("beta", "2.0.0"),
421 ]);
422 let result = index.search(&IndexSearchQuery {
423 query: Some(" ".into()),
424 ..Default::default()
425 });
426 assert_eq!(result.hits.len(), 2);
427 }
428
429 #[test]
430 fn search_name_substring_match() {
431 let index = make_index(vec![
432 make_entry("downsampler", "1.0.0"),
433 make_entry("alerter", "1.0.0"),
434 ]);
435 let result = index.search(&IndexSearchQuery {
436 query: Some("sample".into()),
437 ..Default::default()
438 });
439 assert_eq!(result.hits.len(), 1);
440 assert_eq!(result.hits[0].name.as_str(), "downsampler");
441 }
442
443 #[test]
444 fn search_description_substring_match() {
445 let mut entry = make_entry("notifier", "1.0.0");
446 entry.description = Description::try_new("Fires on every WAL commit").unwrap();
447 let index = make_index(vec![entry, make_entry("other", "1.0.0")]);
448 let result = index.search(&IndexSearchQuery {
449 query: Some("wal".into()),
450 ..Default::default()
451 });
452 assert_eq!(result.hits.len(), 1);
453 assert_eq!(result.hits[0].name.as_str(), "notifier");
454 }
455
456 #[test]
457 fn search_case_insensitive() {
458 let index = make_index(vec![make_entry("downsampler", "1.0.0")]);
459 let result = index.search(&IndexSearchQuery {
460 query: Some("DOWNSAMPLE".into()),
461 ..Default::default()
462 });
463 assert_eq!(result.hits.len(), 1);
464 assert_eq!(result.hits[0].name.as_str(), "downsampler");
465 }
466
467 #[test]
468 fn search_canonical_name_matching() {
469 let index = make_index(vec![make_entry("my-plugin", "1.0.0")]);
471 let result = index.search(&IndexSearchQuery {
472 query: Some("my_plugin".into()),
473 ..Default::default()
474 });
475 assert_eq!(result.hits.len(), 1);
476 assert_eq!(result.hits[0].name.as_str(), "my-plugin");
477
478 let index = make_index(vec![make_entry("my_plugin", "1.0.0")]);
480 let result = index.search(&IndexSearchQuery {
481 query: Some("my-plugin".into()),
482 ..Default::default()
483 });
484 assert_eq!(result.hits.len(), 1);
485 assert_eq!(result.hits[0].name.as_str(), "my_plugin");
486 }
487
488 #[test]
489 fn search_no_dependency_text_search() {
490 let mut entry = make_entry("downsampler", "1.0.0");
491 entry.dependencies.python = vec![PythonRequirement::try_new("requests>=2.31").unwrap()];
492 let index = make_index(vec![entry]);
493 let result = index.search(&IndexSearchQuery {
494 query: Some("requests".into()),
495 ..Default::default()
496 });
497 assert_eq!(result.hits.len(), 0);
498 }
499
500 #[test]
501 fn search_no_url_hash_trigger_text_search() {
502 let mut entry = make_entry("downsampler", "1.0.0");
503 entry.documentation = Some("https://docs.example.com/searchable".parse().unwrap());
504 let index = make_index(vec![entry]);
505 let result = index.search(&IndexSearchQuery {
506 query: Some("searchable".into()),
507 ..Default::default()
508 });
509 assert_eq!(result.hits.len(), 0);
510 }
511
512 #[test]
515 fn search_trigger_type_filter_includes() {
516 let index = make_index(vec![make_entry("writer", "1.0.0")]);
517 let result = index.search(&IndexSearchQuery {
518 trigger_type: Some(TriggerType::ProcessWrites),
519 ..Default::default()
520 });
521 assert_eq!(result.hits.len(), 1);
522 }
523
524 #[test]
525 fn search_trigger_type_filter_excludes() {
526 let mut entry = make_entry("requester", "1.0.0");
527 entry.triggers = vec![TriggerType::ProcessRequest];
528 let index = make_index(vec![entry]);
529 let result = index.search(&IndexSearchQuery {
530 trigger_type: Some(TriggerType::ProcessWrites),
531 ..Default::default()
532 });
533 assert_eq!(result.hits.len(), 0);
534 }
535
536 #[test]
537 fn search_yanked_hidden_by_default() {
538 let mut entry = make_entry("obsolete", "1.0.0");
539 entry.yanked = true;
540 let index = make_index(vec![entry]);
541 let result = index.search(&IndexSearchQuery::default());
542 assert_eq!(result.hits.len(), 0);
543 }
544
545 #[test]
546 fn search_yanked_included_when_requested() {
547 let mut entry = make_entry("obsolete", "1.0.0");
548 entry.yanked = true;
549 let index = make_index(vec![entry]);
550 let result = index.search(&IndexSearchQuery {
551 include_yanked: true,
552 ..Default::default()
553 });
554 assert_eq!(result.hits.len(), 1);
555 assert_matches!(
556 &result.hits[0].visibility,
557 IndexVersionVisibility::Hidden { reasons }
558 if reasons.len() == 1
559 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
560 );
561 }
562
563 #[test]
564 fn search_incompatible_hidden_with_db_version() {
565 let mut entry = make_entry("future", "1.0.0");
566 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
567 let index = make_index(vec![entry]);
568 let result = index.search(&IndexSearchQuery {
569 database_version: Some("3.2.0".parse().unwrap()),
570 ..Default::default()
571 });
572 assert_eq!(result.hits.len(), 0);
573 }
574
575 #[test]
576 fn search_incompatible_included_when_requested() {
577 let mut entry = make_entry("future", "1.0.0");
578 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
579 let index = make_index(vec![entry]);
580 let result = index.search(&IndexSearchQuery {
581 database_version: Some("3.2.0".parse().unwrap()),
582 include_incompatible: true,
583 ..Default::default()
584 });
585 assert_eq!(result.hits.len(), 1);
586 assert_matches!(
587 &result.hits[0].visibility,
588 IndexVersionVisibility::Hidden { reasons }
589 if reasons.len() == 1
590 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. })
591 );
592 }
593
594 #[test]
595 fn search_no_compat_filter_without_db_version() {
596 let mut entry = make_entry("future", "1.0.0");
597 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
598 let index = make_index(vec![entry]);
599 let result = index.search(&IndexSearchQuery::default());
600 assert_eq!(result.hits.len(), 1);
601 assert_eq!(result.hits[0].visibility, IndexVersionVisibility::Visible);
602 }
603
604 #[test]
605 fn search_yanked_and_incompatible_reasons_accumulate() {
606 let mut entry = make_entry("doomed", "1.0.0");
607 entry.yanked = true;
608 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
609 let index = make_index(vec![entry]);
610 let result = index.search(&IndexSearchQuery {
611 database_version: Some("3.2.0".parse().unwrap()),
612 include_yanked: true,
613 include_incompatible: true,
614 ..Default::default()
615 });
616 assert_eq!(result.hits.len(), 1);
617 assert_matches!(
618 &result.hits[0].visibility,
619 IndexVersionVisibility::Hidden { reasons } => {
620 assert_eq!(reasons.len(), 2);
621 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
622 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
623 }
624 );
625 }
626
627 #[test]
630 fn search_one_hit_per_plugin() {
631 let index = make_index(vec![
632 make_entry("alpha", "1.0.0"),
633 make_entry("alpha", "2.0.0"),
634 make_entry("alpha", "3.0.0"),
635 ]);
636 let result = index.search(&IndexSearchQuery::default());
637 assert_eq!(result.hits.len(), 1);
638 }
639
640 #[test]
641 fn search_latest_visible_version_selected() {
642 let index = make_index(vec![
643 make_entry("alpha", "1.0.0"),
644 make_entry("alpha", "1.2.0"),
645 make_entry("alpha", "2.0.0"),
646 ]);
647 let result = index.search(&IndexSearchQuery::default());
648 assert_eq!(result.hits.len(), 1);
649 assert_eq!(
650 result.hits[0].version,
651 "2.0.0".parse::<semver::Version>().unwrap()
652 );
653 }
654
655 #[test]
656 fn search_latest_yanked_skipped() {
657 let mut v2 = make_entry("alpha", "2.0.0");
658 v2.yanked = true;
659 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
660 let result = index.search(&IndexSearchQuery::default());
661 assert_eq!(result.hits.len(), 1);
662 assert_eq!(
663 result.hits[0].version,
664 "1.2.0".parse::<semver::Version>().unwrap()
665 );
666 }
667
668 #[test]
669 fn search_latest_incompatible_skipped() {
670 let mut v2 = make_entry("alpha", "2.0.0");
671 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
672 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
673 let result = index.search(&IndexSearchQuery {
674 database_version: Some("3.2.0".parse().unwrap()),
675 ..Default::default()
676 });
677 assert_eq!(result.hits.len(), 1);
678 assert_eq!(
679 result.hits[0].version,
680 "1.2.0".parse::<semver::Version>().unwrap()
681 );
682 }
683
684 #[test]
685 fn search_hidden_selected_when_included() {
686 let mut v2 = make_entry("alpha", "2.0.0");
687 v2.yanked = true;
688 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
689 let result = index.search(&IndexSearchQuery {
690 include_yanked: true,
691 ..Default::default()
692 });
693 assert_eq!(result.hits.len(), 1);
694 assert_eq!(
695 result.hits[0].version,
696 "2.0.0".parse::<semver::Version>().unwrap()
697 );
698 assert_matches!(
699 &result.hits[0].visibility,
700 IndexVersionVisibility::Hidden { reasons }
701 if reasons.len() == 1
702 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
703 );
704 }
705
706 #[test]
707 fn search_summary_from_selected_version() {
708 let mut v1 = make_entry("alpha", "1.0.0");
709 v1.description = Description::try_new("old description").unwrap();
710 v1.triggers = vec![TriggerType::ProcessRequest];
711 let mut v2 = make_entry("alpha", "2.0.0");
712 v2.description = Description::try_new("new description").unwrap();
713 v2.triggers = vec![
714 TriggerType::ProcessWrites,
715 TriggerType::ProcessScheduledCall,
716 ];
717 let index = make_index(vec![v1, v2]);
718 let result = index.search(&IndexSearchQuery::default());
719 assert_eq!(result.hits.len(), 1);
720 assert_eq!(result.hits[0].description.as_str(), "new description");
721 assert_eq!(result.hits[0].triggers.len(), 2);
722 }
723
724 #[test]
725 fn search_hit_exposes_published_at_from_selected_version() {
726 let index = make_index(vec![
727 make_entry_with_published_at("alpha", "1.0.0", "2026-04-29T18:45:12Z"),
728 make_entry_with_published_at("alpha", "2.0.0", "2027-01-02T03:04:05Z"),
729 ]);
730 let result = index.search(&IndexSearchQuery::default());
731 assert_eq!(result.hits.len(), 1);
732 assert_eq!(result.hits[0].version, semver::Version::new(2, 0, 0));
733 assert_eq!(result.hits[0].published_at.as_str(), "2027-01-02T03:04:05Z");
734 }
735
736 #[test]
737 fn search_hits_sorted_by_canonical_name() {
738 let index = make_index(vec![
739 make_entry("zebra", "1.0.0"),
740 make_entry("alpha", "1.0.0"),
741 make_entry("middle", "1.0.0"),
742 ]);
743 let result = index.search(&IndexSearchQuery::default());
744 let names: Vec<&str> = result.hits.iter().map(|h| h.name.as_str()).collect();
745 assert_eq!(names, vec!["alpha", "middle", "zebra"]);
746 }
747
748 #[test]
749 fn search_semver_precedence_for_selection() {
750 let index = make_index(vec![
751 make_entry("alpha", "1.0.0-alpha"),
752 make_entry("alpha", "1.0.0"),
753 ]);
754 let result = index.search(&IndexSearchQuery::default());
755 assert_eq!(result.hits.len(), 1);
756 assert_eq!(
757 result.hits[0].version,
758 "1.0.0".parse::<semver::Version>().unwrap()
759 );
760 }
761
762 #[test]
763 fn search_build_metadata_deterministic() {
764 let index_a = make_index(vec![
765 make_entry("alpha", "1.0.0+build.1"),
766 make_entry("alpha", "1.0.0+build.2"),
767 ]);
768 let index_b = make_index(vec![
769 make_entry("alpha", "1.0.0+build.2"),
770 make_entry("alpha", "1.0.0+build.1"),
771 ]);
772 let result_a = index_a.search(&IndexSearchQuery::default());
773 let result_b = index_b.search(&IndexSearchQuery::default());
774 assert_eq!(result_a.hits[0].version, result_b.hits[0].version);
775 }
776
777 #[test]
780 fn info_selects_latest_visible() {
781 let index = make_index(vec![
782 make_entry("alpha", "1.0.0"),
783 make_entry("alpha", "1.2.0"),
784 make_entry("alpha", "2.0.0"),
785 ]);
786 let result = index.info(&IndexInfoQuery {
787 name: "alpha".parse().unwrap(),
788 version: None,
789 database_version: None,
790 include_yanked: false,
791 include_incompatible: false,
792 });
793 assert_matches!(&result, IndexInfoResult::Found(info) => {
794 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
795 });
796 }
797
798 #[test]
799 fn info_returns_single_version() {
800 let index = make_index(vec![
801 make_entry("alpha", "1.0.0"),
802 make_entry("alpha", "2.0.0"),
803 ]);
804 let result = index.info(&IndexInfoQuery {
805 name: "alpha".parse().unwrap(),
806 version: None,
807 database_version: None,
808 include_yanked: false,
809 include_incompatible: false,
810 });
811 assert_matches!(&result, IndexInfoResult::Found(_));
812 }
813
814 #[test]
815 fn info_skips_yanked_by_default() {
816 let mut v2 = make_entry("alpha", "2.0.0");
817 v2.yanked = true;
818 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
819 let result = index.info(&IndexInfoQuery {
820 name: "alpha".parse().unwrap(),
821 version: None,
822 database_version: None,
823 include_yanked: false,
824 include_incompatible: false,
825 });
826 assert_matches!(&result, IndexInfoResult::Found(info) => {
827 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
828 });
829 }
830
831 #[test]
832 fn info_skips_incompatible_by_default() {
833 let mut v2 = make_entry("alpha", "2.0.0");
834 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
835 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
836 let result = index.info(&IndexInfoQuery {
837 name: "alpha".parse().unwrap(),
838 version: None,
839 database_version: Some("3.2.0".parse().unwrap()),
840 include_yanked: false,
841 include_incompatible: false,
842 });
843 assert_matches!(&result, IndexInfoResult::Found(info) => {
844 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
845 });
846 }
847
848 #[test]
849 fn info_includes_yanked_when_requested() {
850 let mut v2 = make_entry("alpha", "2.0.0");
851 v2.yanked = true;
852 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
853 let result = index.info(&IndexInfoQuery {
854 name: "alpha".parse().unwrap(),
855 version: None,
856 database_version: None,
857 include_yanked: true,
858 include_incompatible: false,
859 });
860 assert_matches!(&result, IndexInfoResult::Found(info) => {
861 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
862 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
863 if reasons.len() == 1
864 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
865 );
866 });
867 }
868
869 #[test]
870 fn info_includes_incompatible_when_requested() {
871 let mut v2 = make_entry("alpha", "2.0.0");
872 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
873 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
874 let result = index.info(&IndexInfoQuery {
875 name: "alpha".parse().unwrap(),
876 version: None,
877 database_version: Some("3.2.0".parse().unwrap()),
878 include_yanked: false,
879 include_incompatible: true,
880 });
881 assert_matches!(&result, IndexInfoResult::Found(info) => {
882 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
883 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
884 if reasons.len() == 1
885 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. })
886 );
887 });
888 }
889
890 #[test]
891 fn info_no_compat_filter_without_db_version() {
892 let mut entry = make_entry("alpha", "1.0.0");
893 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
894 let index = make_index(vec![entry]);
895 let result = index.info(&IndexInfoQuery {
896 name: "alpha".parse().unwrap(),
897 version: None,
898 database_version: None,
899 include_yanked: false,
900 include_incompatible: false,
901 });
902 assert_matches!(&result, IndexInfoResult::Found(info) => {
903 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
904 });
905 }
906
907 #[test]
908 fn info_missing_name() {
909 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
910 let result = index.info(&IndexInfoQuery {
911 name: "nonexistent".parse().unwrap(),
912 version: None,
913 database_version: None,
914 include_yanked: false,
915 include_incompatible: false,
916 });
917 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
918 assert_eq!(name.as_str(), "nonexistent");
919 assert!(version.is_none());
920 });
921 }
922
923 #[test]
924 fn info_all_yanked() {
925 let mut v1 = make_entry("alpha", "1.0.0");
926 v1.yanked = true;
927 let mut v2 = make_entry("alpha", "2.0.0");
928 v2.yanked = true;
929 let index = make_index(vec![v1, v2]);
930 let result = index.info(&IndexInfoQuery {
931 name: "alpha".parse().unwrap(),
932 version: None,
933 database_version: None,
934 include_yanked: false,
935 include_incompatible: false,
936 });
937 assert_matches!(&result, IndexInfoResult::FilteredOut { name, version, reasons } => {
938 assert_eq!(name.as_str(), "alpha");
939 assert!(version.is_none());
940 assert_eq!(reasons.len(), 1);
941 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
942 });
943 }
944
945 #[test]
946 fn info_all_incompatible() {
947 let mut v1 = make_entry("alpha", "1.0.0");
948 v1.dependencies.database_version = ">=4.0.0".parse().unwrap();
949 let index = make_index(vec![v1]);
950 let result = index.info(&IndexInfoQuery {
951 name: "alpha".parse().unwrap(),
952 version: None,
953 database_version: Some("3.2.0".parse().unwrap()),
954 include_yanked: false,
955 include_incompatible: false,
956 });
957 assert_matches!(&result, IndexInfoResult::FilteredOut { name, version, reasons } => {
958 assert_eq!(name.as_str(), "alpha");
959 assert!(version.is_none());
960 assert_eq!(reasons.len(), 1);
961 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
962 });
963 }
964
965 #[test]
966 fn info_all_hidden_mixed_reasons() {
967 let mut v1 = make_entry("alpha", "1.0.0");
968 v1.yanked = true;
969 let mut v2 = make_entry("alpha", "2.0.0");
970 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
971 let index = make_index(vec![v1, v2]);
972 let result = index.info(&IndexInfoQuery {
973 name: "alpha".parse().unwrap(),
974 version: None,
975 database_version: Some("3.2.0".parse().unwrap()),
976 include_yanked: false,
977 include_incompatible: false,
978 });
979 assert_matches!(&result, IndexInfoResult::FilteredOut { reasons, .. } => {
980 assert_eq!(reasons.len(), 2);
981 assert!(reasons.iter().any(|r| matches!(r, IndexVisibilityReason::Yanked)));
982 assert!(reasons.iter().any(|r| matches!(r, IndexVisibilityReason::IncompatibleDatabaseVersion { .. })));
983 });
984 }
985
986 #[test]
989 fn info_exact_version_visible() {
990 let index = make_index(vec![
991 make_entry("alpha", "1.0.0"),
992 make_entry("alpha", "2.0.0"),
993 ]);
994 let result = index.info(&IndexInfoQuery {
995 name: "alpha".parse().unwrap(),
996 version: Some("1.0.0".parse().unwrap()),
997 database_version: None,
998 include_yanked: false,
999 include_incompatible: false,
1000 });
1001 assert_matches!(&result, IndexInfoResult::Found(info) => {
1002 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
1003 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
1004 });
1005 }
1006
1007 #[test]
1008 fn info_exact_version_yanked() {
1009 let mut entry = make_entry("alpha", "1.0.0");
1010 entry.yanked = true;
1011 let index = make_index(vec![entry]);
1012 let result = index.info(&IndexInfoQuery {
1013 name: "alpha".parse().unwrap(),
1014 version: Some("1.0.0".parse().unwrap()),
1015 database_version: None,
1016 include_yanked: false,
1017 include_incompatible: false,
1018 });
1019 assert_matches!(&result, IndexInfoResult::Found(info) => {
1020 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
1021 if reasons.len() == 1
1022 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
1023 );
1024 });
1025 }
1026
1027 #[test]
1028 fn info_exact_version_incompatible() {
1029 let mut entry = make_entry("alpha", "1.0.0");
1030 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1031 let index = make_index(vec![entry]);
1032 let result = index.info(&IndexInfoQuery {
1033 name: "alpha".parse().unwrap(),
1034 version: Some("1.0.0".parse().unwrap()),
1035 database_version: Some("3.2.0".parse().unwrap()),
1036 include_yanked: false,
1037 include_incompatible: false,
1038 });
1039 assert_matches!(&result, IndexInfoResult::Found(info) => {
1040 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
1041 if reasons.len() == 1
1042 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
1043 required, actual
1044 } if required.to_string() == ">=4.0.0"
1045 && *actual == "3.2.0".parse::<semver::Version>().unwrap()
1046 )
1047 );
1048 });
1049 }
1050
1051 #[test]
1052 fn info_exact_version_yanked_and_incompatible() {
1053 let mut entry = make_entry("alpha", "1.0.0");
1054 entry.yanked = true;
1055 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1056 let index = make_index(vec![entry]);
1057 let result = index.info(&IndexInfoQuery {
1058 name: "alpha".parse().unwrap(),
1059 version: Some("1.0.0".parse().unwrap()),
1060 database_version: Some("3.2.0".parse().unwrap()),
1061 include_yanked: false,
1062 include_incompatible: false,
1063 });
1064 assert_matches!(&result, IndexInfoResult::Found(info) => {
1065 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons } => {
1066 assert_eq!(reasons.len(), 2);
1067 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
1068 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
1069 });
1070 });
1071 }
1072
1073 #[test]
1074 fn info_exact_version_missing() {
1075 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1076 let result = index.info(&IndexInfoQuery {
1077 name: "alpha".parse().unwrap(),
1078 version: Some("9.9.9".parse().unwrap()),
1079 database_version: None,
1080 include_yanked: false,
1081 include_incompatible: false,
1082 });
1083 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
1084 assert_eq!(name.as_str(), "alpha");
1085 assert_eq!(*version, Some("9.9.9".parse::<semver::Version>().unwrap()));
1086 });
1087 }
1088
1089 #[test]
1090 fn info_exact_version_missing_plugin() {
1091 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1092 let result = index.info(&IndexInfoQuery {
1093 name: "nonexistent".parse().unwrap(),
1094 version: Some("1.0.0".parse().unwrap()),
1095 database_version: None,
1096 include_yanked: false,
1097 include_incompatible: false,
1098 });
1099 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
1100 assert_eq!(name.as_str(), "nonexistent");
1101 assert_eq!(*version, Some("1.0.0".parse::<semver::Version>().unwrap()));
1102 });
1103 }
1104
1105 #[test]
1108 fn info_full_metadata() {
1109 let mut entry = make_entry("downsampler", "1.2.0");
1110 entry.description = Description::try_new("Downsamples WAL data").unwrap();
1111 entry.triggers = vec![
1112 TriggerType::ProcessWrites,
1113 TriggerType::ProcessScheduledCall,
1114 ];
1115 entry.homepage = Some("https://example.com".parse().unwrap());
1116 entry.repository = Some("https://github.com/example/ds".parse().unwrap());
1117 entry.documentation = Some("https://docs.example.com/ds".parse().unwrap());
1118 entry.dependencies = Dependencies {
1119 database_version: ">=3.2.0,<4.0.0".parse().unwrap(),
1120 python: vec![PythonRequirement::try_new("requests>=2.31").unwrap()],
1121 plugins: vec![],
1122 };
1123 entry.hash = ArtifactHash::try_new(
1124 "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
1125 )
1126 .unwrap();
1127 let index = make_index(vec![entry]);
1128 let result = index.info(&IndexInfoQuery {
1129 name: "downsampler".parse().unwrap(),
1130 version: None,
1131 database_version: Some("3.5.0".parse().unwrap()),
1132 include_yanked: false,
1133 include_incompatible: false,
1134 });
1135 assert_matches!(&result, IndexInfoResult::Found(info) => {
1136 assert_eq!(info.name.as_str(), "downsampler");
1137 assert_eq!(info.version, "1.2.0".parse::<semver::Version>().unwrap());
1138 assert_eq!(info.description.as_str(), "Downsamples WAL data");
1139 assert_eq!(info.triggers.len(), 2);
1140 assert!(info.homepage.is_some());
1141 assert!(info.repository.is_some());
1142 assert!(info.documentation.is_some());
1143 assert_eq!(info.dependencies.database_version.to_string(), ">=3.2.0, <4.0.0");
1144 assert_eq!(info.dependencies.python.len(), 1);
1145 assert!(info.hash.as_str().starts_with("sha256:abcdef"));
1146 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
1147 });
1148 }
1149
1150 #[test]
1151 fn info_exposes_published_at() {
1152 let index = make_index(vec![make_entry_with_published_at(
1153 "alpha",
1154 "1.0.0",
1155 "2027-01-02T03:04:05Z",
1156 )]);
1157 let result = index.info(&IndexInfoQuery {
1158 name: "alpha".parse().unwrap(),
1159 version: Some("1.0.0".parse().unwrap()),
1160 database_version: None,
1161 include_yanked: false,
1162 include_incompatible: false,
1163 });
1164 assert_matches!(&result, IndexInfoResult::Found(info) => {
1165 assert_eq!(info.published_at.as_str(), "2027-01-02T03:04:05Z");
1166 });
1167 }
1168
1169 #[test]
1170 fn info_incompatible_reason_includes_versions() {
1171 let mut entry = make_entry("alpha", "1.0.0");
1172 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1173 let index = make_index(vec![entry]);
1174 let result = index.info(&IndexInfoQuery {
1175 name: "alpha".parse().unwrap(),
1176 version: Some("1.0.0".parse().unwrap()),
1177 database_version: Some("3.2.0".parse().unwrap()),
1178 include_yanked: false,
1179 include_incompatible: false,
1180 });
1181 assert_matches!(&result, IndexInfoResult::Found(info) => {
1182 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons } => {
1183 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
1184 required, actual
1185 } => {
1186 assert_eq!(required.to_string(), ">=4.0.0");
1187 assert_eq!(*actual, "3.2.0".parse::<semver::Version>().unwrap());
1188 });
1189 });
1190 });
1191 }
1192
1193 #[test]
1196 fn search_result_serializes() {
1197 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1198 let result = index.search(&IndexSearchQuery::default());
1199 let json = serde_json::to_value(&result).unwrap();
1200 assert!(json["hits"].is_array());
1201 assert_eq!(json["hits"][0]["name"], "alpha");
1202 assert_eq!(json["hits"][0]["version"], "1.0.0");
1203 assert_eq!(json["hits"][0]["published_at"], "2026-04-29T18:45:12Z");
1204 assert!(json["hits"][0]["description"].is_string());
1205 assert!(json["hits"][0]["triggers"].is_array());
1206 assert_eq!(json["hits"][0]["visibility"], "Visible");
1207 }
1208
1209 #[test]
1210 fn info_found_serializes() {
1211 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1212 let result = index.info(&IndexInfoQuery {
1213 name: "alpha".parse().unwrap(),
1214 version: None,
1215 database_version: None,
1216 include_yanked: false,
1217 include_incompatible: false,
1218 });
1219 let json = serde_json::to_value(&result).unwrap();
1220 let found = &json["Found"];
1221 assert_eq!(found["name"], "alpha");
1222 assert_eq!(found["version"], "1.0.0");
1223 assert_eq!(found["published_at"], "2026-04-29T18:45:12Z");
1224 assert!(found["description"].is_string());
1225 assert!(found["triggers"].is_array());
1226 assert!(found["dependencies"].is_object());
1227 assert!(found["hash"].is_string());
1228 assert_eq!(found["visibility"], "Visible");
1229 }
1230
1231 #[test]
1232 fn info_not_found_serializes() {
1233 let index = make_index(vec![]);
1234 let result = index.info(&IndexInfoQuery {
1235 name: "missing".parse().unwrap(),
1236 version: Some("1.0.0".parse().unwrap()),
1237 database_version: None,
1238 include_yanked: false,
1239 include_incompatible: false,
1240 });
1241 let json = serde_json::to_value(&result).unwrap();
1242 let not_found = &json["NotFound"];
1243 assert_eq!(not_found["name"], "missing");
1244 assert_eq!(not_found["version"], "1.0.0");
1245 }
1246
1247 #[test]
1248 fn info_filtered_out_serializes() {
1249 let mut entry = make_entry("alpha", "1.0.0");
1250 entry.yanked = true;
1251 let index = make_index(vec![entry]);
1252 let result = index.info(&IndexInfoQuery {
1253 name: "alpha".parse().unwrap(),
1254 version: None,
1255 database_version: None,
1256 include_yanked: false,
1257 include_incompatible: false,
1258 });
1259 let json = serde_json::to_value(&result).unwrap();
1260 let filtered = &json["FilteredOut"];
1261 assert_eq!(filtered["name"], "alpha");
1262 assert!(filtered["version"].is_null());
1263 assert!(filtered["reasons"].is_array());
1264 }
1265
1266 #[test]
1267 fn visibility_reasons_serialize() {
1268 let yanked = IndexVisibilityReason::Yanked;
1269 let incompat = IndexVisibilityReason::IncompatibleDatabaseVersion {
1270 required: ">=4.0.0".parse().unwrap(),
1271 actual: "3.2.0".parse().unwrap(),
1272 };
1273 let yanked_json = serde_json::to_value(&yanked).unwrap();
1274 let incompat_json = serde_json::to_value(&incompat).unwrap();
1275 assert_eq!(yanked_json, "Yanked");
1276 assert!(incompat_json["IncompatibleDatabaseVersion"].is_object());
1277 assert_eq!(
1278 incompat_json["IncompatibleDatabaseVersion"]["required"],
1279 ">=4.0.0"
1280 );
1281 assert_eq!(
1282 incompat_json["IncompatibleDatabaseVersion"]["actual"],
1283 "3.2.0"
1284 );
1285 }
1286
1287 #[test]
1290 fn search_empty_index() {
1291 let index = make_index(vec![]);
1292 let result = index.search(&IndexSearchQuery::default());
1293 assert_eq!(result.hits.len(), 0);
1294 }
1295
1296 #[test]
1297 fn info_empty_index() {
1298 let index = make_index(vec![]);
1299 let result = index.info(&IndexInfoQuery {
1300 name: "alpha".parse().unwrap(),
1301 version: None,
1302 database_version: None,
1303 include_yanked: false,
1304 include_incompatible: false,
1305 });
1306 assert_matches!(&result, IndexInfoResult::NotFound { .. });
1307 }
1308
1309 #[test]
1310 fn search_only_hidden_not_shown() {
1311 let mut entry = make_entry("alpha", "1.0.0");
1312 entry.yanked = true;
1313 let index = make_index(vec![entry]);
1314 let result = index.search(&IndexSearchQuery::default());
1315 assert_eq!(result.hits.len(), 0);
1316 }
1317
1318 #[test]
1319 fn search_mixed_visible_hidden_uses_visible() {
1320 let mut v2 = make_entry("alpha", "2.0.0");
1321 v2.yanked = true;
1322 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
1323 let result = index.search(&IndexSearchQuery::default());
1324 assert_eq!(result.hits.len(), 1);
1325 assert_eq!(
1326 result.hits[0].version,
1327 "1.0.0".parse::<semver::Version>().unwrap()
1328 );
1329 assert_eq!(result.hits[0].visibility, IndexVersionVisibility::Visible);
1330 }
1331
1332 #[test]
1333 fn search_mixed_with_include_flags() {
1334 let mut v2 = make_entry("alpha", "2.0.0");
1335 v2.yanked = true;
1336 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
1337 let result = index.search(&IndexSearchQuery {
1338 include_yanked: true,
1339 ..Default::default()
1340 });
1341 assert_eq!(result.hits.len(), 1);
1342 assert_eq!(
1343 result.hits[0].version,
1344 "2.0.0".parse::<semver::Version>().unwrap()
1345 );
1346 }
1347
1348 #[test]
1349 fn search_text_match_on_hidden_visible_older_matches() {
1350 let mut v2 = make_entry("alpha", "2.0.0");
1351 v2.yanked = true;
1352 v2.description = Description::try_new("common in both versions").unwrap();
1353 let mut v1 = make_entry("alpha", "1.0.0");
1354 v1.description = Description::try_new("common desc").unwrap();
1355 let index = make_index(vec![v1, v2]);
1356 let result = index.search(&IndexSearchQuery {
1359 query: Some("common".into()),
1360 ..Default::default()
1361 });
1362 assert_eq!(result.hits.len(), 1);
1363 assert_eq!(
1364 result.hits[0].version,
1365 "1.0.0".parse::<semver::Version>().unwrap()
1366 );
1367 }
1368
1369 #[test]
1370 fn search_trigger_filter_before_grouping() {
1371 let mut v2 = make_entry("alpha", "2.0.0");
1372 v2.triggers = vec![TriggerType::ProcessRequest];
1373 let mut v1 = make_entry("alpha", "1.0.0");
1374 v1.triggers = vec![TriggerType::ProcessWrites];
1375 let index = make_index(vec![v1, v2]);
1376 let result = index.search(&IndexSearchQuery {
1377 trigger_type: Some(TriggerType::ProcessWrites),
1378 ..Default::default()
1379 });
1380 assert_eq!(result.hits.len(), 1);
1381 assert_eq!(
1382 result.hits[0].version,
1383 "1.0.0".parse::<semver::Version>().unwrap()
1384 );
1385 assert_eq!(result.hits[0].triggers, vec![TriggerType::ProcessWrites]);
1386 }
1387}