* feat: added sections for advanced mods * fix: treat a null bundle file type as normal * chore: drop ScrollArea::lazy_grid * chore: pass one header height to lazy_sections * chore: name BundleFileType::Normal in test fixtures * feat: don't offer advanced bundle mods as optional mods --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
488 lines
13 KiB
Rust
488 lines
13 KiB
Rust
use oneclient_common::domain::{ContentType, GameLoader, ProviderId};
|
|
use oneclient_content::bundles::{
|
|
BundleFile, BundleFileKind, BundleFileType, BundleManifest, check_bundle_updates,
|
|
};
|
|
use oneclient_core::LauncherState;
|
|
use oneclient_core::clusters::CreateClusterOptions;
|
|
use oneclient_db::dao::{artifact as artifact_dao, cluster_bundle as bundle_dao};
|
|
use oneclient_db::models::OverrideType;
|
|
|
|
const BUNDLE: &str = "Test Bundle";
|
|
const MC_VERSION: &str = "1.21.1";
|
|
const PROJECT_ID: &str = "sodium";
|
|
const VERSION_ID: &str = "v1";
|
|
const HASH: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
|
|
fn managed_file(enabled: bool) -> BundleFile {
|
|
BundleFile {
|
|
enabled,
|
|
hidden: false,
|
|
path: "mods/sodium.jar".to_string(),
|
|
size: 1,
|
|
file_type: BundleFileType::Normal,
|
|
kind: BundleFileKind::Managed {
|
|
provider: ProviderId::Modrinth,
|
|
project_id: PROJECT_ID.to_string(),
|
|
version_id: VERSION_ID.to_string(),
|
|
sha1: HASH.to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Added by the catalog after the last sync untracked so it has no override
|
|
fn newly_shipped_file() -> BundleFile {
|
|
BundleFile {
|
|
enabled: true,
|
|
hidden: false,
|
|
path: "mods/newcomer.jar".to_string(),
|
|
size: 1,
|
|
file_type: BundleFileType::Normal,
|
|
kind: BundleFileKind::Managed {
|
|
provider: ProviderId::Modrinth,
|
|
project_id: "newcomer".to_string(),
|
|
version_id: "v1".to_string(),
|
|
sha1: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn manifest(files: Vec<BundleFile>) -> BundleManifest {
|
|
BundleManifest {
|
|
name: BUNDLE.to_string(),
|
|
version_id: "1".to_string(),
|
|
category: "test".to_string(),
|
|
mc_version: MC_VERSION.to_string(),
|
|
loader: GameLoader::Fabric,
|
|
loader_version: "0.16.0".to_string(),
|
|
enabled: true,
|
|
files,
|
|
}
|
|
}
|
|
|
|
async fn cluster_with_tracked_mod(state: &LauncherState) -> i64 {
|
|
let global = state.settings.read().global_game_settings.clone();
|
|
let cluster = state
|
|
.clusters
|
|
.create(
|
|
&global,
|
|
CreateClusterOptions::new("Bundle Cluster", MC_VERSION, GameLoader::Fabric),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
artifact_dao::insert_artifact(
|
|
&state.services.db,
|
|
HASH,
|
|
ContentType::Mod as i64,
|
|
"artifacts/sodium.jar",
|
|
"sodium.jar",
|
|
Some(1),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
artifact_dao::link_cluster_artifact(&state.services.db, cluster.id, HASH, "sodium.jar")
|
|
.await
|
|
.unwrap();
|
|
|
|
artifact_dao::upsert_provider_release(
|
|
&state.services.db,
|
|
ProviderId::Modrinth as i64,
|
|
PROJECT_ID,
|
|
VERSION_ID,
|
|
HASH,
|
|
"Sodium",
|
|
"1.0.0",
|
|
None,
|
|
MC_VERSION,
|
|
"fabric",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
bundle_dao::track_bundle_artifact(
|
|
&state.services.db,
|
|
cluster.id,
|
|
HASH,
|
|
BUNDLE,
|
|
VERSION_ID,
|
|
PROJECT_ID,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
cluster.id
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mod_still_in_manifest_is_not_removed() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(&state, manifest(vec![managed_file(true)]))
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.removals_available.is_empty(),
|
|
"an unchanged tracked mod was flagged for removal: {:?}",
|
|
check.removals_available
|
|
);
|
|
assert!(check.updates_available.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mod_dropped_from_manifest_is_removed() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(&state, manifest(vec![]))
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
check.removals_available.len(),
|
|
1,
|
|
"a mod the bundle no longer ships should be removed"
|
|
);
|
|
assert_eq!(check.removals_available[0].package_id, PROJECT_ID);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabled_mod_dropped_from_manifest_is_still_removed() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(&state, manifest(vec![]))
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
|
.await
|
|
.unwrap();
|
|
bundle_dao::save_override(
|
|
&state.services.db,
|
|
cluster_id,
|
|
BUNDLE,
|
|
PROJECT_ID,
|
|
OverrideType::Disabled,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
check.removals_available.len(),
|
|
1,
|
|
"a disabled mod the bundle no longer ships should still be removed"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn user_disabled_mod_is_not_treated_as_a_removal() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(&state, manifest(vec![managed_file(true)]))
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
|
.await
|
|
.unwrap();
|
|
bundle_dao::save_override(
|
|
&state.services.db,
|
|
cluster_id,
|
|
BUNDLE,
|
|
PROJECT_ID,
|
|
OverrideType::Disabled,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.removals_available.is_empty(),
|
|
"a user-disabled mod is still shipped by the bundle and must not be removed: {:?}",
|
|
check.removals_available
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn live_bundle_takes_on_new_catalog_files() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(
|
|
&state,
|
|
manifest(vec![managed_file(true), newly_shipped_file()]),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
check.additions_available.len(),
|
|
1,
|
|
"a bundle the user still has enabled should pick up newly shipped files"
|
|
);
|
|
assert_eq!(
|
|
check.additions_available[0].new_file.kind.package_id(),
|
|
"newcomer"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn emptied_bundle_does_not_take_on_new_catalog_files() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(
|
|
&state,
|
|
manifest(vec![managed_file(true), newly_shipped_file()]),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
// Disabling everything is how you opt out
|
|
// The newcomer lacks an override only
|
|
// because it did not exist yet that absence must not read as consent
|
|
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
|
.await
|
|
.unwrap();
|
|
bundle_dao::save_override(
|
|
&state.services.db,
|
|
cluster_id,
|
|
BUNDLE,
|
|
PROJECT_ID,
|
|
OverrideType::Disabled,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.additions_available.is_empty(),
|
|
"a bundle whose content the user disabled must not reinstate itself: {:?}",
|
|
check
|
|
.additions_available
|
|
.iter()
|
|
.map(|a| a.new_file.kind.package_id())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn removed_bundle_content_does_not_take_on_new_catalog_files() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(
|
|
&state,
|
|
manifest(vec![managed_file(true), newly_shipped_file()]),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
oneclient_content::bundles::remove_artifact_from_cluster(
|
|
cluster_id,
|
|
HASH,
|
|
true,
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.additions_available.is_empty(),
|
|
"a bundle the user emptied by removal must not reinstate itself: {:?}",
|
|
check
|
|
.additions_available
|
|
.iter()
|
|
.map(|a| a.new_file.kind.package_id())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn opting_a_single_file_in_keeps_the_bundle_live() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
oneclient_core::dev::seed_bundle_archive(
|
|
&state,
|
|
manifest(vec![managed_file(true), newly_shipped_file()]),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
|
.await
|
|
.unwrap();
|
|
bundle_dao::save_override(
|
|
&state.services.db,
|
|
cluster_id,
|
|
BUNDLE,
|
|
PROJECT_ID,
|
|
OverrideType::Disabled,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
bundle_dao::save_override(
|
|
&state.services.db,
|
|
cluster_id,
|
|
BUNDLE,
|
|
"newcomer",
|
|
OverrideType::Enabled,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
check.additions_available.len(),
|
|
1,
|
|
"an explicit opt-in is consent, even with the rest of the bundle disabled"
|
|
);
|
|
}
|
|
|
|
async fn seed_delisted_bundle(state: &LauncherState) {
|
|
oneclient_db::dao::bundle::upsert_bundle(
|
|
&state.services.db,
|
|
oneclient_db::models::NewBundle {
|
|
remote_path: "bundles/delisted.mrpack",
|
|
mc_version: MC_VERSION,
|
|
mc_loader: GameLoader::Fabric as i64,
|
|
file_name: "delisted.mrpack",
|
|
name: Some(BUNDLE),
|
|
version_id: Some("1"),
|
|
category: Some("test"),
|
|
loader_version: Some("0.16.0"),
|
|
disk_path: "bundles/delisted.mrpack",
|
|
hidden: true,
|
|
etag: None,
|
|
synced_at: None,
|
|
},
|
|
)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delisted_bundle_content_is_removed() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
seed_delisted_bundle(&state).await;
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
check.removals_available.len(),
|
|
1,
|
|
"content exclusive to a bundle the catalog dropped should be removed"
|
|
);
|
|
assert_eq!(check.removals_available[0].package_id, PROJECT_ID);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delisted_bundle_content_another_bundle_still_ships_is_kept() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
seed_delisted_bundle(&state).await;
|
|
let mut successor = manifest(vec![managed_file(true)]);
|
|
successor.name = "Successor Bundle".to_string();
|
|
oneclient_core::dev::seed_bundle_archive(&state, successor)
|
|
.await
|
|
.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.removals_available.is_empty(),
|
|
"a mod another live bundle still ships is not exclusive: {:?}",
|
|
check.removals_available
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tracked_bundle_that_never_synced_is_not_removed() {
|
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
|
|
|
let check = check_bundle_updates(
|
|
cluster_id,
|
|
state.bundles.as_ref(),
|
|
&state.services.content(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
check.removals_available.is_empty(),
|
|
"an absent catalog is not a delisting and must not take content down: {:?}",
|
|
check.removals_available
|
|
);
|
|
}
|