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 },
330 hash: ArtifactHash::try_new(
331 "sha256:0000000000000000000000000000000000000000000000000000000000000000",
332 )
333 .unwrap(),
334 yanked: false,
335 }
336 }
337
338 fn make_index(entries: Vec<IndexEntry>) -> Index {
339 Index {
340 index_schema_version: IndexSchemaVersion::CURRENT,
341 artifacts_url: ArtifactsUrl::try_new("https://example.com/artifacts").unwrap(),
342 plugins: entries,
343 }
344 }
345
346 #[test]
349 fn visibility_visible_when_not_yanked_and_compatible() {
350 let entry = make_entry("foo", "1.0.0");
351 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
352 assert_eq!(vis, IndexVersionVisibility::Visible);
353 }
354
355 #[test]
356 fn visibility_hidden_when_yanked() {
357 let mut entry = make_entry("foo", "1.0.0");
358 entry.yanked = true;
359 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
360 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
361 assert_eq!(reasons.len(), 1);
362 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
363 });
364 }
365
366 #[test]
367 fn visibility_hidden_when_incompatible() {
368 let mut entry = make_entry("foo", "1.0.0");
369 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
370 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
371 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
372 assert_eq!(reasons.len(), 1);
373 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
374 required, actual
375 } => {
376 assert_eq!(required.to_string(), ">=4.0.0");
377 assert_eq!(*actual, "3.2.0".parse::<semver::Version>().unwrap());
378 });
379 });
380 }
381
382 #[test]
383 fn visibility_hidden_yanked_and_incompatible() {
384 let mut entry = make_entry("foo", "1.0.0");
385 entry.yanked = true;
386 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
387 let vis = visibility_for(&entry, Some(&"3.2.0".parse().unwrap()));
388 assert_matches!(&vis, IndexVersionVisibility::Hidden { reasons } => {
389 assert_eq!(reasons.len(), 2);
390 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
391 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
392 });
393 }
394
395 #[test]
396 fn visibility_no_compat_check_without_db_version() {
397 let mut entry = make_entry("foo", "1.0.0");
398 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
399 let vis = visibility_for(&entry, None);
400 assert_eq!(vis, IndexVersionVisibility::Visible);
401 }
402
403 #[test]
406 fn search_empty_query_matches_all() {
407 let index = make_index(vec![
408 make_entry("alpha", "1.0.0"),
409 make_entry("beta", "2.0.0"),
410 ]);
411 let result = index.search(&IndexSearchQuery::default());
412 assert_eq!(result.hits.len(), 2);
413 }
414
415 #[test]
416 fn search_whitespace_query_matches_all() {
417 let index = make_index(vec![
418 make_entry("alpha", "1.0.0"),
419 make_entry("beta", "2.0.0"),
420 ]);
421 let result = index.search(&IndexSearchQuery {
422 query: Some(" ".into()),
423 ..Default::default()
424 });
425 assert_eq!(result.hits.len(), 2);
426 }
427
428 #[test]
429 fn search_name_substring_match() {
430 let index = make_index(vec![
431 make_entry("downsampler", "1.0.0"),
432 make_entry("alerter", "1.0.0"),
433 ]);
434 let result = index.search(&IndexSearchQuery {
435 query: Some("sample".into()),
436 ..Default::default()
437 });
438 assert_eq!(result.hits.len(), 1);
439 assert_eq!(result.hits[0].name.as_str(), "downsampler");
440 }
441
442 #[test]
443 fn search_description_substring_match() {
444 let mut entry = make_entry("notifier", "1.0.0");
445 entry.description = Description::try_new("Fires on every WAL commit").unwrap();
446 let index = make_index(vec![entry, make_entry("other", "1.0.0")]);
447 let result = index.search(&IndexSearchQuery {
448 query: Some("wal".into()),
449 ..Default::default()
450 });
451 assert_eq!(result.hits.len(), 1);
452 assert_eq!(result.hits[0].name.as_str(), "notifier");
453 }
454
455 #[test]
456 fn search_case_insensitive() {
457 let index = make_index(vec![make_entry("downsampler", "1.0.0")]);
458 let result = index.search(&IndexSearchQuery {
459 query: Some("DOWNSAMPLE".into()),
460 ..Default::default()
461 });
462 assert_eq!(result.hits.len(), 1);
463 assert_eq!(result.hits[0].name.as_str(), "downsampler");
464 }
465
466 #[test]
467 fn search_canonical_name_matching() {
468 let index = make_index(vec![make_entry("my-plugin", "1.0.0")]);
470 let result = index.search(&IndexSearchQuery {
471 query: Some("my_plugin".into()),
472 ..Default::default()
473 });
474 assert_eq!(result.hits.len(), 1);
475 assert_eq!(result.hits[0].name.as_str(), "my-plugin");
476
477 let index = make_index(vec![make_entry("my_plugin", "1.0.0")]);
479 let result = index.search(&IndexSearchQuery {
480 query: Some("my-plugin".into()),
481 ..Default::default()
482 });
483 assert_eq!(result.hits.len(), 1);
484 assert_eq!(result.hits[0].name.as_str(), "my_plugin");
485 }
486
487 #[test]
488 fn search_no_dependency_text_search() {
489 let mut entry = make_entry("downsampler", "1.0.0");
490 entry.dependencies.python = vec![PythonRequirement::try_new("requests>=2.31").unwrap()];
491 let index = make_index(vec![entry]);
492 let result = index.search(&IndexSearchQuery {
493 query: Some("requests".into()),
494 ..Default::default()
495 });
496 assert_eq!(result.hits.len(), 0);
497 }
498
499 #[test]
500 fn search_no_url_hash_trigger_text_search() {
501 let mut entry = make_entry("downsampler", "1.0.0");
502 entry.documentation = Some("https://docs.example.com/searchable".parse().unwrap());
503 let index = make_index(vec![entry]);
504 let result = index.search(&IndexSearchQuery {
505 query: Some("searchable".into()),
506 ..Default::default()
507 });
508 assert_eq!(result.hits.len(), 0);
509 }
510
511 #[test]
514 fn search_trigger_type_filter_includes() {
515 let index = make_index(vec![make_entry("writer", "1.0.0")]);
516 let result = index.search(&IndexSearchQuery {
517 trigger_type: Some(TriggerType::ProcessWrites),
518 ..Default::default()
519 });
520 assert_eq!(result.hits.len(), 1);
521 }
522
523 #[test]
524 fn search_trigger_type_filter_excludes() {
525 let mut entry = make_entry("requester", "1.0.0");
526 entry.triggers = vec![TriggerType::ProcessRequest];
527 let index = make_index(vec![entry]);
528 let result = index.search(&IndexSearchQuery {
529 trigger_type: Some(TriggerType::ProcessWrites),
530 ..Default::default()
531 });
532 assert_eq!(result.hits.len(), 0);
533 }
534
535 #[test]
536 fn search_yanked_hidden_by_default() {
537 let mut entry = make_entry("obsolete", "1.0.0");
538 entry.yanked = true;
539 let index = make_index(vec![entry]);
540 let result = index.search(&IndexSearchQuery::default());
541 assert_eq!(result.hits.len(), 0);
542 }
543
544 #[test]
545 fn search_yanked_included_when_requested() {
546 let mut entry = make_entry("obsolete", "1.0.0");
547 entry.yanked = true;
548 let index = make_index(vec![entry]);
549 let result = index.search(&IndexSearchQuery {
550 include_yanked: true,
551 ..Default::default()
552 });
553 assert_eq!(result.hits.len(), 1);
554 assert_matches!(
555 &result.hits[0].visibility,
556 IndexVersionVisibility::Hidden { reasons }
557 if reasons.len() == 1
558 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
559 );
560 }
561
562 #[test]
563 fn search_incompatible_hidden_with_db_version() {
564 let mut entry = make_entry("future", "1.0.0");
565 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
566 let index = make_index(vec![entry]);
567 let result = index.search(&IndexSearchQuery {
568 database_version: Some("3.2.0".parse().unwrap()),
569 ..Default::default()
570 });
571 assert_eq!(result.hits.len(), 0);
572 }
573
574 #[test]
575 fn search_incompatible_included_when_requested() {
576 let mut entry = make_entry("future", "1.0.0");
577 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
578 let index = make_index(vec![entry]);
579 let result = index.search(&IndexSearchQuery {
580 database_version: Some("3.2.0".parse().unwrap()),
581 include_incompatible: true,
582 ..Default::default()
583 });
584 assert_eq!(result.hits.len(), 1);
585 assert_matches!(
586 &result.hits[0].visibility,
587 IndexVersionVisibility::Hidden { reasons }
588 if reasons.len() == 1
589 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. })
590 );
591 }
592
593 #[test]
594 fn search_no_compat_filter_without_db_version() {
595 let mut entry = make_entry("future", "1.0.0");
596 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
597 let index = make_index(vec![entry]);
598 let result = index.search(&IndexSearchQuery::default());
599 assert_eq!(result.hits.len(), 1);
600 assert_eq!(result.hits[0].visibility, IndexVersionVisibility::Visible);
601 }
602
603 #[test]
604 fn search_yanked_and_incompatible_reasons_accumulate() {
605 let mut entry = make_entry("doomed", "1.0.0");
606 entry.yanked = true;
607 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
608 let index = make_index(vec![entry]);
609 let result = index.search(&IndexSearchQuery {
610 database_version: Some("3.2.0".parse().unwrap()),
611 include_yanked: true,
612 include_incompatible: true,
613 ..Default::default()
614 });
615 assert_eq!(result.hits.len(), 1);
616 assert_matches!(
617 &result.hits[0].visibility,
618 IndexVersionVisibility::Hidden { reasons } => {
619 assert_eq!(reasons.len(), 2);
620 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
621 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
622 }
623 );
624 }
625
626 #[test]
629 fn search_one_hit_per_plugin() {
630 let index = make_index(vec![
631 make_entry("alpha", "1.0.0"),
632 make_entry("alpha", "2.0.0"),
633 make_entry("alpha", "3.0.0"),
634 ]);
635 let result = index.search(&IndexSearchQuery::default());
636 assert_eq!(result.hits.len(), 1);
637 }
638
639 #[test]
640 fn search_latest_visible_version_selected() {
641 let index = make_index(vec![
642 make_entry("alpha", "1.0.0"),
643 make_entry("alpha", "1.2.0"),
644 make_entry("alpha", "2.0.0"),
645 ]);
646 let result = index.search(&IndexSearchQuery::default());
647 assert_eq!(result.hits.len(), 1);
648 assert_eq!(
649 result.hits[0].version,
650 "2.0.0".parse::<semver::Version>().unwrap()
651 );
652 }
653
654 #[test]
655 fn search_latest_yanked_skipped() {
656 let mut v2 = make_entry("alpha", "2.0.0");
657 v2.yanked = true;
658 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
659 let result = index.search(&IndexSearchQuery::default());
660 assert_eq!(result.hits.len(), 1);
661 assert_eq!(
662 result.hits[0].version,
663 "1.2.0".parse::<semver::Version>().unwrap()
664 );
665 }
666
667 #[test]
668 fn search_latest_incompatible_skipped() {
669 let mut v2 = make_entry("alpha", "2.0.0");
670 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
671 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
672 let result = index.search(&IndexSearchQuery {
673 database_version: Some("3.2.0".parse().unwrap()),
674 ..Default::default()
675 });
676 assert_eq!(result.hits.len(), 1);
677 assert_eq!(
678 result.hits[0].version,
679 "1.2.0".parse::<semver::Version>().unwrap()
680 );
681 }
682
683 #[test]
684 fn search_hidden_selected_when_included() {
685 let mut v2 = make_entry("alpha", "2.0.0");
686 v2.yanked = true;
687 let index = make_index(vec![make_entry("alpha", "1.2.0"), v2]);
688 let result = index.search(&IndexSearchQuery {
689 include_yanked: true,
690 ..Default::default()
691 });
692 assert_eq!(result.hits.len(), 1);
693 assert_eq!(
694 result.hits[0].version,
695 "2.0.0".parse::<semver::Version>().unwrap()
696 );
697 assert_matches!(
698 &result.hits[0].visibility,
699 IndexVersionVisibility::Hidden { reasons }
700 if reasons.len() == 1
701 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
702 );
703 }
704
705 #[test]
706 fn search_summary_from_selected_version() {
707 let mut v1 = make_entry("alpha", "1.0.0");
708 v1.description = Description::try_new("old description").unwrap();
709 v1.triggers = vec![TriggerType::ProcessRequest];
710 let mut v2 = make_entry("alpha", "2.0.0");
711 v2.description = Description::try_new("new description").unwrap();
712 v2.triggers = vec![
713 TriggerType::ProcessWrites,
714 TriggerType::ProcessScheduledCall,
715 ];
716 let index = make_index(vec![v1, v2]);
717 let result = index.search(&IndexSearchQuery::default());
718 assert_eq!(result.hits.len(), 1);
719 assert_eq!(result.hits[0].description.as_str(), "new description");
720 assert_eq!(result.hits[0].triggers.len(), 2);
721 }
722
723 #[test]
724 fn search_hit_exposes_published_at_from_selected_version() {
725 let index = make_index(vec![
726 make_entry_with_published_at("alpha", "1.0.0", "2026-04-29T18:45:12Z"),
727 make_entry_with_published_at("alpha", "2.0.0", "2027-01-02T03:04:05Z"),
728 ]);
729 let result = index.search(&IndexSearchQuery::default());
730 assert_eq!(result.hits.len(), 1);
731 assert_eq!(result.hits[0].version, semver::Version::new(2, 0, 0));
732 assert_eq!(result.hits[0].published_at.as_str(), "2027-01-02T03:04:05Z");
733 }
734
735 #[test]
736 fn search_hits_sorted_by_canonical_name() {
737 let index = make_index(vec![
738 make_entry("zebra", "1.0.0"),
739 make_entry("alpha", "1.0.0"),
740 make_entry("middle", "1.0.0"),
741 ]);
742 let result = index.search(&IndexSearchQuery::default());
743 let names: Vec<&str> = result.hits.iter().map(|h| h.name.as_str()).collect();
744 assert_eq!(names, vec!["alpha", "middle", "zebra"]);
745 }
746
747 #[test]
748 fn search_semver_precedence_for_selection() {
749 let index = make_index(vec![
750 make_entry("alpha", "1.0.0-alpha"),
751 make_entry("alpha", "1.0.0"),
752 ]);
753 let result = index.search(&IndexSearchQuery::default());
754 assert_eq!(result.hits.len(), 1);
755 assert_eq!(
756 result.hits[0].version,
757 "1.0.0".parse::<semver::Version>().unwrap()
758 );
759 }
760
761 #[test]
762 fn search_build_metadata_deterministic() {
763 let index_a = make_index(vec![
764 make_entry("alpha", "1.0.0+build.1"),
765 make_entry("alpha", "1.0.0+build.2"),
766 ]);
767 let index_b = make_index(vec![
768 make_entry("alpha", "1.0.0+build.2"),
769 make_entry("alpha", "1.0.0+build.1"),
770 ]);
771 let result_a = index_a.search(&IndexSearchQuery::default());
772 let result_b = index_b.search(&IndexSearchQuery::default());
773 assert_eq!(result_a.hits[0].version, result_b.hits[0].version);
774 }
775
776 #[test]
779 fn info_selects_latest_visible() {
780 let index = make_index(vec![
781 make_entry("alpha", "1.0.0"),
782 make_entry("alpha", "1.2.0"),
783 make_entry("alpha", "2.0.0"),
784 ]);
785 let result = index.info(&IndexInfoQuery {
786 name: "alpha".parse().unwrap(),
787 version: None,
788 database_version: None,
789 include_yanked: false,
790 include_incompatible: false,
791 });
792 assert_matches!(&result, IndexInfoResult::Found(info) => {
793 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
794 });
795 }
796
797 #[test]
798 fn info_returns_single_version() {
799 let index = make_index(vec![
800 make_entry("alpha", "1.0.0"),
801 make_entry("alpha", "2.0.0"),
802 ]);
803 let result = index.info(&IndexInfoQuery {
804 name: "alpha".parse().unwrap(),
805 version: None,
806 database_version: None,
807 include_yanked: false,
808 include_incompatible: false,
809 });
810 assert_matches!(&result, IndexInfoResult::Found(_));
811 }
812
813 #[test]
814 fn info_skips_yanked_by_default() {
815 let mut v2 = make_entry("alpha", "2.0.0");
816 v2.yanked = true;
817 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
818 let result = index.info(&IndexInfoQuery {
819 name: "alpha".parse().unwrap(),
820 version: None,
821 database_version: None,
822 include_yanked: false,
823 include_incompatible: false,
824 });
825 assert_matches!(&result, IndexInfoResult::Found(info) => {
826 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
827 });
828 }
829
830 #[test]
831 fn info_skips_incompatible_by_default() {
832 let mut v2 = make_entry("alpha", "2.0.0");
833 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
834 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
835 let result = index.info(&IndexInfoQuery {
836 name: "alpha".parse().unwrap(),
837 version: None,
838 database_version: Some("3.2.0".parse().unwrap()),
839 include_yanked: false,
840 include_incompatible: false,
841 });
842 assert_matches!(&result, IndexInfoResult::Found(info) => {
843 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
844 });
845 }
846
847 #[test]
848 fn info_includes_yanked_when_requested() {
849 let mut v2 = make_entry("alpha", "2.0.0");
850 v2.yanked = true;
851 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
852 let result = index.info(&IndexInfoQuery {
853 name: "alpha".parse().unwrap(),
854 version: None,
855 database_version: None,
856 include_yanked: true,
857 include_incompatible: false,
858 });
859 assert_matches!(&result, IndexInfoResult::Found(info) => {
860 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
861 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
862 if reasons.len() == 1
863 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
864 );
865 });
866 }
867
868 #[test]
869 fn info_includes_incompatible_when_requested() {
870 let mut v2 = make_entry("alpha", "2.0.0");
871 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
872 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
873 let result = index.info(&IndexInfoQuery {
874 name: "alpha".parse().unwrap(),
875 version: None,
876 database_version: Some("3.2.0".parse().unwrap()),
877 include_yanked: false,
878 include_incompatible: true,
879 });
880 assert_matches!(&result, IndexInfoResult::Found(info) => {
881 assert_eq!(info.version, "2.0.0".parse::<semver::Version>().unwrap());
882 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
883 if reasons.len() == 1
884 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. })
885 );
886 });
887 }
888
889 #[test]
890 fn info_no_compat_filter_without_db_version() {
891 let mut entry = make_entry("alpha", "1.0.0");
892 entry.dependencies.database_version = ">=99.0.0".parse().unwrap();
893 let index = make_index(vec![entry]);
894 let result = index.info(&IndexInfoQuery {
895 name: "alpha".parse().unwrap(),
896 version: None,
897 database_version: None,
898 include_yanked: false,
899 include_incompatible: false,
900 });
901 assert_matches!(&result, IndexInfoResult::Found(info) => {
902 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
903 });
904 }
905
906 #[test]
907 fn info_missing_name() {
908 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
909 let result = index.info(&IndexInfoQuery {
910 name: "nonexistent".parse().unwrap(),
911 version: None,
912 database_version: None,
913 include_yanked: false,
914 include_incompatible: false,
915 });
916 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
917 assert_eq!(name.as_str(), "nonexistent");
918 assert!(version.is_none());
919 });
920 }
921
922 #[test]
923 fn info_all_yanked() {
924 let mut v1 = make_entry("alpha", "1.0.0");
925 v1.yanked = true;
926 let mut v2 = make_entry("alpha", "2.0.0");
927 v2.yanked = true;
928 let index = make_index(vec![v1, v2]);
929 let result = index.info(&IndexInfoQuery {
930 name: "alpha".parse().unwrap(),
931 version: None,
932 database_version: None,
933 include_yanked: false,
934 include_incompatible: false,
935 });
936 assert_matches!(&result, IndexInfoResult::FilteredOut { name, version, reasons } => {
937 assert_eq!(name.as_str(), "alpha");
938 assert!(version.is_none());
939 assert_eq!(reasons.len(), 1);
940 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
941 });
942 }
943
944 #[test]
945 fn info_all_incompatible() {
946 let mut v1 = make_entry("alpha", "1.0.0");
947 v1.dependencies.database_version = ">=4.0.0".parse().unwrap();
948 let index = make_index(vec![v1]);
949 let result = index.info(&IndexInfoQuery {
950 name: "alpha".parse().unwrap(),
951 version: None,
952 database_version: Some("3.2.0".parse().unwrap()),
953 include_yanked: false,
954 include_incompatible: false,
955 });
956 assert_matches!(&result, IndexInfoResult::FilteredOut { name, version, reasons } => {
957 assert_eq!(name.as_str(), "alpha");
958 assert!(version.is_none());
959 assert_eq!(reasons.len(), 1);
960 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
961 });
962 }
963
964 #[test]
965 fn info_all_hidden_mixed_reasons() {
966 let mut v1 = make_entry("alpha", "1.0.0");
967 v1.yanked = true;
968 let mut v2 = make_entry("alpha", "2.0.0");
969 v2.dependencies.database_version = ">=4.0.0".parse().unwrap();
970 let index = make_index(vec![v1, v2]);
971 let result = index.info(&IndexInfoQuery {
972 name: "alpha".parse().unwrap(),
973 version: None,
974 database_version: Some("3.2.0".parse().unwrap()),
975 include_yanked: false,
976 include_incompatible: false,
977 });
978 assert_matches!(&result, IndexInfoResult::FilteredOut { reasons, .. } => {
979 assert_eq!(reasons.len(), 2);
980 assert!(reasons.iter().any(|r| matches!(r, IndexVisibilityReason::Yanked)));
981 assert!(reasons.iter().any(|r| matches!(r, IndexVisibilityReason::IncompatibleDatabaseVersion { .. })));
982 });
983 }
984
985 #[test]
988 fn info_exact_version_visible() {
989 let index = make_index(vec![
990 make_entry("alpha", "1.0.0"),
991 make_entry("alpha", "2.0.0"),
992 ]);
993 let result = index.info(&IndexInfoQuery {
994 name: "alpha".parse().unwrap(),
995 version: Some("1.0.0".parse().unwrap()),
996 database_version: None,
997 include_yanked: false,
998 include_incompatible: false,
999 });
1000 assert_matches!(&result, IndexInfoResult::Found(info) => {
1001 assert_eq!(info.version, "1.0.0".parse::<semver::Version>().unwrap());
1002 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
1003 });
1004 }
1005
1006 #[test]
1007 fn info_exact_version_yanked() {
1008 let mut entry = make_entry("alpha", "1.0.0");
1009 entry.yanked = true;
1010 let index = make_index(vec![entry]);
1011 let result = index.info(&IndexInfoQuery {
1012 name: "alpha".parse().unwrap(),
1013 version: Some("1.0.0".parse().unwrap()),
1014 database_version: None,
1015 include_yanked: false,
1016 include_incompatible: false,
1017 });
1018 assert_matches!(&result, IndexInfoResult::Found(info) => {
1019 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
1020 if reasons.len() == 1
1021 && matches!(&reasons[0], IndexVisibilityReason::Yanked)
1022 );
1023 });
1024 }
1025
1026 #[test]
1027 fn info_exact_version_incompatible() {
1028 let mut entry = make_entry("alpha", "1.0.0");
1029 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1030 let index = make_index(vec![entry]);
1031 let result = index.info(&IndexInfoQuery {
1032 name: "alpha".parse().unwrap(),
1033 version: Some("1.0.0".parse().unwrap()),
1034 database_version: Some("3.2.0".parse().unwrap()),
1035 include_yanked: false,
1036 include_incompatible: false,
1037 });
1038 assert_matches!(&result, IndexInfoResult::Found(info) => {
1039 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons }
1040 if reasons.len() == 1
1041 && matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
1042 required, actual
1043 } if required.to_string() == ">=4.0.0"
1044 && *actual == "3.2.0".parse::<semver::Version>().unwrap()
1045 )
1046 );
1047 });
1048 }
1049
1050 #[test]
1051 fn info_exact_version_yanked_and_incompatible() {
1052 let mut entry = make_entry("alpha", "1.0.0");
1053 entry.yanked = true;
1054 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1055 let index = make_index(vec![entry]);
1056 let result = index.info(&IndexInfoQuery {
1057 name: "alpha".parse().unwrap(),
1058 version: Some("1.0.0".parse().unwrap()),
1059 database_version: Some("3.2.0".parse().unwrap()),
1060 include_yanked: false,
1061 include_incompatible: false,
1062 });
1063 assert_matches!(&result, IndexInfoResult::Found(info) => {
1064 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons } => {
1065 assert_eq!(reasons.len(), 2);
1066 assert_matches!(&reasons[0], IndexVisibilityReason::Yanked);
1067 assert_matches!(&reasons[1], IndexVisibilityReason::IncompatibleDatabaseVersion { .. });
1068 });
1069 });
1070 }
1071
1072 #[test]
1073 fn info_exact_version_missing() {
1074 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1075 let result = index.info(&IndexInfoQuery {
1076 name: "alpha".parse().unwrap(),
1077 version: Some("9.9.9".parse().unwrap()),
1078 database_version: None,
1079 include_yanked: false,
1080 include_incompatible: false,
1081 });
1082 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
1083 assert_eq!(name.as_str(), "alpha");
1084 assert_eq!(*version, Some("9.9.9".parse::<semver::Version>().unwrap()));
1085 });
1086 }
1087
1088 #[test]
1089 fn info_exact_version_missing_plugin() {
1090 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1091 let result = index.info(&IndexInfoQuery {
1092 name: "nonexistent".parse().unwrap(),
1093 version: Some("1.0.0".parse().unwrap()),
1094 database_version: None,
1095 include_yanked: false,
1096 include_incompatible: false,
1097 });
1098 assert_matches!(&result, IndexInfoResult::NotFound { name, version } => {
1099 assert_eq!(name.as_str(), "nonexistent");
1100 assert_eq!(*version, Some("1.0.0".parse::<semver::Version>().unwrap()));
1101 });
1102 }
1103
1104 #[test]
1107 fn info_full_metadata() {
1108 let mut entry = make_entry("downsampler", "1.2.0");
1109 entry.description = Description::try_new("Downsamples WAL data").unwrap();
1110 entry.triggers = vec![
1111 TriggerType::ProcessWrites,
1112 TriggerType::ProcessScheduledCall,
1113 ];
1114 entry.homepage = Some("https://example.com".parse().unwrap());
1115 entry.repository = Some("https://github.com/example/ds".parse().unwrap());
1116 entry.documentation = Some("https://docs.example.com/ds".parse().unwrap());
1117 entry.dependencies = Dependencies {
1118 database_version: ">=3.2.0,<4.0.0".parse().unwrap(),
1119 python: vec![PythonRequirement::try_new("requests>=2.31").unwrap()],
1120 };
1121 entry.hash = ArtifactHash::try_new(
1122 "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
1123 )
1124 .unwrap();
1125 let index = make_index(vec![entry]);
1126 let result = index.info(&IndexInfoQuery {
1127 name: "downsampler".parse().unwrap(),
1128 version: None,
1129 database_version: Some("3.5.0".parse().unwrap()),
1130 include_yanked: false,
1131 include_incompatible: false,
1132 });
1133 assert_matches!(&result, IndexInfoResult::Found(info) => {
1134 assert_eq!(info.name.as_str(), "downsampler");
1135 assert_eq!(info.version, "1.2.0".parse::<semver::Version>().unwrap());
1136 assert_eq!(info.description.as_str(), "Downsamples WAL data");
1137 assert_eq!(info.triggers.len(), 2);
1138 assert!(info.homepage.is_some());
1139 assert!(info.repository.is_some());
1140 assert!(info.documentation.is_some());
1141 assert_eq!(info.dependencies.database_version.to_string(), ">=3.2.0, <4.0.0");
1142 assert_eq!(info.dependencies.python.len(), 1);
1143 assert!(info.hash.as_str().starts_with("sha256:abcdef"));
1144 assert_eq!(info.visibility, IndexVersionVisibility::Visible);
1145 });
1146 }
1147
1148 #[test]
1149 fn info_exposes_published_at() {
1150 let index = make_index(vec![make_entry_with_published_at(
1151 "alpha",
1152 "1.0.0",
1153 "2027-01-02T03:04:05Z",
1154 )]);
1155 let result = index.info(&IndexInfoQuery {
1156 name: "alpha".parse().unwrap(),
1157 version: Some("1.0.0".parse().unwrap()),
1158 database_version: None,
1159 include_yanked: false,
1160 include_incompatible: false,
1161 });
1162 assert_matches!(&result, IndexInfoResult::Found(info) => {
1163 assert_eq!(info.published_at.as_str(), "2027-01-02T03:04:05Z");
1164 });
1165 }
1166
1167 #[test]
1168 fn info_incompatible_reason_includes_versions() {
1169 let mut entry = make_entry("alpha", "1.0.0");
1170 entry.dependencies.database_version = ">=4.0.0".parse().unwrap();
1171 let index = make_index(vec![entry]);
1172 let result = index.info(&IndexInfoQuery {
1173 name: "alpha".parse().unwrap(),
1174 version: Some("1.0.0".parse().unwrap()),
1175 database_version: Some("3.2.0".parse().unwrap()),
1176 include_yanked: false,
1177 include_incompatible: false,
1178 });
1179 assert_matches!(&result, IndexInfoResult::Found(info) => {
1180 assert_matches!(&info.visibility, IndexVersionVisibility::Hidden { reasons } => {
1181 assert_matches!(&reasons[0], IndexVisibilityReason::IncompatibleDatabaseVersion {
1182 required, actual
1183 } => {
1184 assert_eq!(required.to_string(), ">=4.0.0");
1185 assert_eq!(*actual, "3.2.0".parse::<semver::Version>().unwrap());
1186 });
1187 });
1188 });
1189 }
1190
1191 #[test]
1194 fn search_result_serializes() {
1195 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1196 let result = index.search(&IndexSearchQuery::default());
1197 let json = serde_json::to_value(&result).unwrap();
1198 assert!(json["hits"].is_array());
1199 assert_eq!(json["hits"][0]["name"], "alpha");
1200 assert_eq!(json["hits"][0]["version"], "1.0.0");
1201 assert_eq!(json["hits"][0]["published_at"], "2026-04-29T18:45:12Z");
1202 assert!(json["hits"][0]["description"].is_string());
1203 assert!(json["hits"][0]["triggers"].is_array());
1204 assert_eq!(json["hits"][0]["visibility"], "Visible");
1205 }
1206
1207 #[test]
1208 fn info_found_serializes() {
1209 let index = make_index(vec![make_entry("alpha", "1.0.0")]);
1210 let result = index.info(&IndexInfoQuery {
1211 name: "alpha".parse().unwrap(),
1212 version: None,
1213 database_version: None,
1214 include_yanked: false,
1215 include_incompatible: false,
1216 });
1217 let json = serde_json::to_value(&result).unwrap();
1218 let found = &json["Found"];
1219 assert_eq!(found["name"], "alpha");
1220 assert_eq!(found["version"], "1.0.0");
1221 assert_eq!(found["published_at"], "2026-04-29T18:45:12Z");
1222 assert!(found["description"].is_string());
1223 assert!(found["triggers"].is_array());
1224 assert!(found["dependencies"].is_object());
1225 assert!(found["hash"].is_string());
1226 assert_eq!(found["visibility"], "Visible");
1227 }
1228
1229 #[test]
1230 fn info_not_found_serializes() {
1231 let index = make_index(vec![]);
1232 let result = index.info(&IndexInfoQuery {
1233 name: "missing".parse().unwrap(),
1234 version: Some("1.0.0".parse().unwrap()),
1235 database_version: None,
1236 include_yanked: false,
1237 include_incompatible: false,
1238 });
1239 let json = serde_json::to_value(&result).unwrap();
1240 let not_found = &json["NotFound"];
1241 assert_eq!(not_found["name"], "missing");
1242 assert_eq!(not_found["version"], "1.0.0");
1243 }
1244
1245 #[test]
1246 fn info_filtered_out_serializes() {
1247 let mut entry = make_entry("alpha", "1.0.0");
1248 entry.yanked = true;
1249 let index = make_index(vec![entry]);
1250 let result = index.info(&IndexInfoQuery {
1251 name: "alpha".parse().unwrap(),
1252 version: None,
1253 database_version: None,
1254 include_yanked: false,
1255 include_incompatible: false,
1256 });
1257 let json = serde_json::to_value(&result).unwrap();
1258 let filtered = &json["FilteredOut"];
1259 assert_eq!(filtered["name"], "alpha");
1260 assert!(filtered["version"].is_null());
1261 assert!(filtered["reasons"].is_array());
1262 }
1263
1264 #[test]
1265 fn visibility_reasons_serialize() {
1266 let yanked = IndexVisibilityReason::Yanked;
1267 let incompat = IndexVisibilityReason::IncompatibleDatabaseVersion {
1268 required: ">=4.0.0".parse().unwrap(),
1269 actual: "3.2.0".parse().unwrap(),
1270 };
1271 let yanked_json = serde_json::to_value(&yanked).unwrap();
1272 let incompat_json = serde_json::to_value(&incompat).unwrap();
1273 assert_eq!(yanked_json, "Yanked");
1274 assert!(incompat_json["IncompatibleDatabaseVersion"].is_object());
1275 assert_eq!(
1276 incompat_json["IncompatibleDatabaseVersion"]["required"],
1277 ">=4.0.0"
1278 );
1279 assert_eq!(
1280 incompat_json["IncompatibleDatabaseVersion"]["actual"],
1281 "3.2.0"
1282 );
1283 }
1284
1285 #[test]
1288 fn search_empty_index() {
1289 let index = make_index(vec![]);
1290 let result = index.search(&IndexSearchQuery::default());
1291 assert_eq!(result.hits.len(), 0);
1292 }
1293
1294 #[test]
1295 fn info_empty_index() {
1296 let index = make_index(vec![]);
1297 let result = index.info(&IndexInfoQuery {
1298 name: "alpha".parse().unwrap(),
1299 version: None,
1300 database_version: None,
1301 include_yanked: false,
1302 include_incompatible: false,
1303 });
1304 assert_matches!(&result, IndexInfoResult::NotFound { .. });
1305 }
1306
1307 #[test]
1308 fn search_only_hidden_not_shown() {
1309 let mut entry = make_entry("alpha", "1.0.0");
1310 entry.yanked = true;
1311 let index = make_index(vec![entry]);
1312 let result = index.search(&IndexSearchQuery::default());
1313 assert_eq!(result.hits.len(), 0);
1314 }
1315
1316 #[test]
1317 fn search_mixed_visible_hidden_uses_visible() {
1318 let mut v2 = make_entry("alpha", "2.0.0");
1319 v2.yanked = true;
1320 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
1321 let result = index.search(&IndexSearchQuery::default());
1322 assert_eq!(result.hits.len(), 1);
1323 assert_eq!(
1324 result.hits[0].version,
1325 "1.0.0".parse::<semver::Version>().unwrap()
1326 );
1327 assert_eq!(result.hits[0].visibility, IndexVersionVisibility::Visible);
1328 }
1329
1330 #[test]
1331 fn search_mixed_with_include_flags() {
1332 let mut v2 = make_entry("alpha", "2.0.0");
1333 v2.yanked = true;
1334 let index = make_index(vec![make_entry("alpha", "1.0.0"), v2]);
1335 let result = index.search(&IndexSearchQuery {
1336 include_yanked: true,
1337 ..Default::default()
1338 });
1339 assert_eq!(result.hits.len(), 1);
1340 assert_eq!(
1341 result.hits[0].version,
1342 "2.0.0".parse::<semver::Version>().unwrap()
1343 );
1344 }
1345
1346 #[test]
1347 fn search_text_match_on_hidden_visible_older_matches() {
1348 let mut v2 = make_entry("alpha", "2.0.0");
1349 v2.yanked = true;
1350 v2.description = Description::try_new("common in both versions").unwrap();
1351 let mut v1 = make_entry("alpha", "1.0.0");
1352 v1.description = Description::try_new("common desc").unwrap();
1353 let index = make_index(vec![v1, v2]);
1354 let result = index.search(&IndexSearchQuery {
1357 query: Some("common".into()),
1358 ..Default::default()
1359 });
1360 assert_eq!(result.hits.len(), 1);
1361 assert_eq!(
1362 result.hits[0].version,
1363 "1.0.0".parse::<semver::Version>().unwrap()
1364 );
1365 }
1366
1367 #[test]
1368 fn search_trigger_filter_before_grouping() {
1369 let mut v2 = make_entry("alpha", "2.0.0");
1370 v2.triggers = vec![TriggerType::ProcessRequest];
1371 let mut v1 = make_entry("alpha", "1.0.0");
1372 v1.triggers = vec![TriggerType::ProcessWrites];
1373 let index = make_index(vec![v1, v2]);
1374 let result = index.search(&IndexSearchQuery {
1375 trigger_type: Some(TriggerType::ProcessWrites),
1376 ..Default::default()
1377 });
1378 assert_eq!(result.hits.len(), 1);
1379 assert_eq!(
1380 result.hits[0].version,
1381 "1.0.0".parse::<semver::Version>().unwrap()
1382 );
1383 assert_eq!(result.hits[0].triggers, vec![TriggerType::ProcessWrites]);
1384 }
1385}