Skip to content

Commit f7912c6

Browse files
committed
🧪 test(pypi): exercise shadow read faults
An oversized repository route stopped reaching the shadow query after startup began enforcing repository persistence errors. Corrupt a valid stored decision through the metadata migration boundary so both fixtures exercise the intended decode failure through the public HTTP route.
1 parent 31c60c7 commit f7912c6

2 files changed

Lines changed: 76 additions & 26 deletions

File tree

crates/peryx-ecosystem-pypi/tests/system-suite/tests/cases/shadow.rs

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use peryx_ecosystem_pypi::upload::Uploaded;
1212
use peryx_ecosystem_pypi::{CoreMetadata, File, Provenance, Yanked};
1313
use peryx_identity::{GrantScope, Role};
1414
use peryx_policy::{PolicyAction, PolicyDecisionState};
15-
use peryx_storage::meta::NewPolicyDecision;
15+
use peryx_storage::meta::{
16+
LegacyMetadataSource, MetadataMigration, MetadataRecord, MetadataRecordSet, NewPolicyDecision,
17+
};
1618
use tower::ServiceExt as _;
1719

1820
use crate::config::{Config, IndexConfig, IndexKind, SecretSource};
@@ -163,23 +165,6 @@ async fn seeded_state() -> (tempfile::TempDir, Arc<AppState>) {
163165
(dir, state)
164166
}
165167

166-
async fn seeded_state_named(repository: &str) -> (tempfile::TempDir, Arc<AppState>) {
167-
let dir = tempfile::tempdir().unwrap();
168-
let mut root = virtual_root();
169-
root.name = repository.to_owned();
170-
root.route = repository.to_owned();
171-
let config = Config {
172-
data_dir: dir.path().to_path_buf(),
173-
indexes: vec![cached_pypi(), hosted(), root],
174-
..Config::default()
175-
};
176-
let state = build_state(&config).unwrap();
177-
provision_admin(&state).await;
178-
seed_hosted(&state);
179-
seed_cached(&state);
180-
(dir, state)
181-
}
182-
183168
async fn app() -> (tempfile::TempDir, axum::Router) {
184169
let (dir, state) = seeded_state().await;
185170
(dir, router_for(state))
@@ -339,13 +324,21 @@ async fn test_a_denied_filename_carries_its_decision_on_every_member_row() {
339324

340325
#[tokio::test]
341326
async fn test_a_failed_decision_read_surfaces_as_a_server_error() {
342-
let repository = "r".repeat(513);
343-
let (_dir, state) = seeded_state_named(&repository).await;
327+
let (_dir, state) = seeded_state().await;
328+
record_decision(
329+
&state,
330+
Some(HOSTED_FILE),
331+
PolicyAction::Serve,
332+
PolicyDecisionState::Allow,
333+
"read-fault",
334+
None,
335+
);
336+
state.serving.meta.migrate_metadata(&UnreadableDecision).unwrap();
344337
let router = router_for(state);
345338

346339
let (status, _headers, body) = get(
347340
&router,
348-
&format!("/+shadow/candidates?repository={repository}&project=acme-pkg"),
341+
"/+shadow/candidates?repository=root/pypi&project=acme-pkg",
349342
Some(("Alice", PASSWORD)),
350343
)
351344
.await;
@@ -355,6 +348,33 @@ async fn test_a_failed_decision_read_surfaces_as_a_server_error() {
355348
assert_eq!(document["error"], "shadow query failed");
356349
}
357350

351+
struct UnreadableDecision;
352+
353+
impl MetadataMigration for UnreadableDecision {
354+
fn name(&self) -> &'static str {
355+
"unreadable-decision"
356+
}
357+
358+
fn record_sets(&self) -> &[MetadataRecordSet] {
359+
&[MetadataRecordSet::PolicyDecisionHistory]
360+
}
361+
362+
fn legacy_sources(&self) -> &[LegacyMetadataSource] {
363+
&[]
364+
}
365+
366+
fn rewrite(
367+
&self,
368+
_record_set: MetadataRecordSet,
369+
record: &MetadataRecord,
370+
) -> Result<Option<MetadataRecord>, String> {
371+
Ok(Some(MetadataRecord {
372+
key: record.key.clone(),
373+
value: b"{".to_vec(),
374+
}))
375+
}
376+
}
377+
358378
#[tokio::test]
359379
async fn test_a_waiting_filename_reports_its_retry_window() {
360380
let (_dir, state) = seeded_state().await;

crates/peryx-ecosystem-pypi/tests/unit/shadow/coverage_tests.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use peryx_identity::{Action, Glob, Grant, GrantScope, IndexAcl, NamedToken, Role
1212
use peryx_index::{Index, IndexKind};
1313
use peryx_policy::{Policy, PolicyAction, PolicyDecisionState};
1414
use peryx_storage::blob::BlobStorage;
15-
use peryx_storage::meta::{MetaStore, NewPolicyDecision};
15+
use peryx_storage::meta::{
16+
LegacyMetadataSource, MetaStore, MetadataMigration, MetadataRecord, MetadataRecordSet, NewPolicyDecision,
17+
};
1618
use redb::TableDefinition;
1719
use rstest::rstest;
1820
use tower::ServiceExt as _;
@@ -347,13 +349,14 @@ async fn shadow_contract_reports_malformed_parameters() {
347349

348350
#[tokio::test]
349351
async fn shadow_candidates_hide_decision_store_errors() {
350-
let repository = "r".repeat(513);
351-
let (_directory, state) = seeded_state(&repository, &repository, IndexAcl::default());
352-
let authorization = local_reader(&state, &repository).await;
352+
let (_directory, state) = seeded_state("root-pypi", "root/pypi", IndexAcl::default());
353+
record_decisions(&state);
354+
state.serving.meta.migrate_metadata(&UnreadableDecision).unwrap();
355+
let authorization = local_reader(&state, "root-pypi").await;
353356

354357
let (status, _, body) = request(
355358
&state,
356-
&format!("/+shadow/candidates?repository={repository}&project={PROJECT}"),
359+
"/+shadow/candidates?repository=root/pypi&project=acme-pkg",
357360
Some(HeaderValue::from_str(&authorization).unwrap()),
358361
)
359362
.await;
@@ -365,6 +368,33 @@ async fn shadow_candidates_hide_decision_store_errors() {
365368
);
366369
}
367370

371+
struct UnreadableDecision;
372+
373+
impl MetadataMigration for UnreadableDecision {
374+
fn name(&self) -> &'static str {
375+
"unreadable-decision"
376+
}
377+
378+
fn record_sets(&self) -> &[MetadataRecordSet] {
379+
&[MetadataRecordSet::PolicyDecisionHistory]
380+
}
381+
382+
fn legacy_sources(&self) -> &[LegacyMetadataSource] {
383+
&[]
384+
}
385+
386+
fn rewrite(
387+
&self,
388+
_record_set: MetadataRecordSet,
389+
record: &MetadataRecord,
390+
) -> Result<Option<MetadataRecord>, String> {
391+
Ok(Some(MetadataRecord {
392+
key: record.key.clone(),
393+
value: b"{".to_vec(),
394+
}))
395+
}
396+
}
397+
368398
#[rstest]
369399
#[case::missing(None)]
370400
#[case::wrong_scheme(Some(HeaderValue::from_static("Bearer token")))]

0 commit comments

Comments
 (0)