diff --git a/crates/turborepo-lockfiles/src/bun/mod.rs b/crates/turborepo-lockfiles/src/bun/mod.rs index be0eb352b3240..25629cc115072 100644 --- a/crates/turborepo-lockfiles/src/bun/mod.rs +++ b/crates/turborepo-lockfiles/src/bun/mod.rs @@ -577,4 +577,268 @@ mod test { let lockfile = BunLockfile::from_str(&contents); assert!(lockfile.is_err(), "matching packages have differing shas"); } + + #[test] + fn test_subgraph_with_empty_workspace_packages() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test subgraph with no workspace packages + let subgraph = lockfile.subgraph(&[], &["turbo@2.3.3".into()]).unwrap(); + let subgraph_data = subgraph.lockfile().unwrap(); + + // Should only contain root workspace + assert_eq!(subgraph_data.workspaces.len(), 1); + assert!(subgraph_data.workspaces.contains_key("")); + + // Should contain the requested package + assert!(subgraph_data + .packages + .values() + .any(|p| p.ident == "turbo@2.3.3")); + } + + #[test] + fn test_subgraph_with_missing_package() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test subgraph with non-existent package + let subgraph = lockfile + .subgraph(&["apps/docs".into()], &["nonexistent-package".into()]) + .unwrap(); + let subgraph_data = subgraph.lockfile().unwrap(); + + // Should not contain the non-existent package + assert!(!subgraph_data + .packages + .values() + .any(|p| p.ident == "nonexistent-package")); + + // But should still include the workspace + assert!(subgraph_data.workspaces.contains_key("apps/docs")); + } + + #[test] + fn test_resolve_package_with_invalid_workspace() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test with invalid workspace + let result = lockfile.resolve_package("invalid/workspace", "is-odd", "3.0.1"); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + crate::Error::MissingWorkspace(_) + )); + } + + #[test] + fn test_resolve_package_with_nonexistent_package() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test with nonexistent package + let result = lockfile + .resolve_package("apps/docs", "nonexistent-package", "1.0.0") + .unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_all_dependencies_with_invalid_key() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test with invalid package key + let result = lockfile.all_dependencies("invalid-package-key"); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + crate::Error::MissingPackage(_) + )); + } + + #[test] + fn test_global_change_detection() { + let lockfile1 = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + let lockfile2 = BunLockfile::from_str(PATCH_LOCKFILE).unwrap(); + + // Current implementation only returns true if package manager types differ + // Both lockfiles are BunLockfile, so this returns false + assert!(!lockfile1.global_change(&lockfile2)); + + // Same lockfile should not show global change + assert!(!lockfile1.global_change(&lockfile1)); + } + + #[test] + fn test_turbo_version_detection() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + let turbo_version = lockfile.turbo_version(); + assert_eq!(turbo_version, Some("2.3.3".to_string())); + } + + #[test] + fn test_turbo_version_missing() { + // Create a lockfile without turbo + let contents = serde_json::to_string(&json!({ + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test" + } + }, + "packages": {} + })) + .unwrap(); + let lockfile = BunLockfile::from_str(&contents).unwrap(); + + let turbo_version = lockfile.turbo_version(); + assert!(turbo_version.is_none()); + } + + #[test] + fn test_human_name_generation() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + let package = crate::Package::new("is-odd", "3.0.1"); + let human_name = lockfile.human_name(&package); + assert_eq!(human_name, Some("is-odd@3.0.1".to_string())); + + // Test with nonexistent package + let nonexistent = crate::Package::new("nonexistent", "1.0.0"); + let human_name = lockfile.human_name(&nonexistent); + assert!(human_name.is_none()); + } + + #[test] + fn test_encode_decode_roundtrip() { + let original = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test encode/decode roundtrip + let encoded = original.encode().unwrap(); + let decoded = BunLockfile::from_bytes(&encoded).unwrap(); + + // Should preserve basic structure + assert_eq!( + original.data.lockfile_version, + decoded.data.lockfile_version + ); + assert_eq!( + original.data.workspaces.len(), + decoded.data.workspaces.len() + ); + assert_eq!(original.data.packages.len(), decoded.data.packages.len()); + } + + #[test] + fn test_optional_peer_dependencies() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test that optional peer dependencies are handled correctly + let all_deps = lockfile + .all_dependencies("@turbo/gen@1.13.4") + .unwrap() + .unwrap(); + + // Should include regular dependencies but may skip optional peers + assert!(all_deps.len() > 0); + + // Verify no missing package errors for optional peers + for (dep_key, _) in &all_deps { + // All returned dependencies should exist in the lockfile + assert!( + lockfile.data.packages.contains_key(dep_key) + || lockfile.package_entry(dep_key).is_some() + ); + } + } + + #[test] + fn test_package_version_extraction() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test version extraction from different package formats + for (_, entry) in &lockfile.data.packages { + let version = entry.version(); + // Version should be non-empty and valid + assert!(!version.is_empty()); + + // Should extract version after @ symbol + if entry.ident.contains('@') { + assert!(entry.ident.ends_with(version)); + } + } + } + + #[test] + fn test_subgraph_preserves_patches() { + let lockfile = BunLockfile::from_str(PATCH_LOCKFILE).unwrap(); + + let subgraph = lockfile + .subgraph(&["apps/b".into()], &["is-odd@3.0.0".into()]) + .unwrap(); + let subgraph_data = subgraph.lockfile().unwrap(); + + // Should preserve patches for included packages + assert!(subgraph_data + .patched_dependencies + .contains_key("is-odd@3.0.0")); + assert_eq!( + subgraph_data.patched_dependencies.get("is-odd@3.0.0"), + Some(&"patches/is-odd@3.0.0.patch".to_string()) + ); + } + + #[test] + fn test_workspace_dependency_types() { + let lockfile = BunLockfile::from_str(BASIC_LOCKFILE).unwrap(); + + // Test that different dependency types are handled correctly + for (_, workspace) in &lockfile.data.workspaces { + // Verify that dependencies maps exist and are accessible + if let Some(_deps) = &workspace.dependencies { + // Dependencies can be empty + } + if let Some(_dev_deps) = &workspace.dev_dependencies { + // Dev dependencies can be empty + } + if let Some(_opt_deps) = &workspace.optional_dependencies { + // Optional dependencies can be empty + } + } + } + + #[test] + fn test_malformed_json_handling() { + let malformed_json = r#" + { + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test" + } + }, + "packages": { + "invalid": ["incomplete + "#; + + let result = BunLockfile::from_str(malformed_json); + assert!(result.is_err()); + } + + #[test] + fn test_trailing_commas_handling() { + let json_with_trailing_commas = r#" + { + "lockfileVersion": 0, + "workspaces": { + "": { + "name": "test", + } + }, + "packages": {}, + } + "#; + + let result = BunLockfile::from_str(json_with_trailing_commas); + assert!(result.is_ok(), "Should handle trailing commas gracefully"); + } } diff --git a/crates/turborepo-lockfiles/src/npm.rs b/crates/turborepo-lockfiles/src/npm.rs index 55508052e6d1f..0f3cc41cf55d4 100644 --- a/crates/turborepo-lockfiles/src/npm.rs +++ b/crates/turborepo-lockfiles/src/npm.rs @@ -469,4 +469,80 @@ mod test { assert_eq!(lockfile.turbo_version().as_deref(), Some("1.5.5")); Ok(()) } + + #[test] + fn test_subgraph_with_empty_inputs() -> Result<(), Error> { + let lockfile = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + // Test with empty workspace packages and packages + let subgraph = lockfile.subgraph(&[], &[])?; + let subgraph_npm = subgraph.as_ref() as &dyn Any; + let subgraph_npm = subgraph_npm.downcast_ref::().unwrap(); + + // Should contain root package if it exists + assert!(subgraph_npm.packages.contains_key("")); + assert_eq!(subgraph_npm.packages.len(), 1); + + Ok(()) + } + + #[test] + fn test_resolve_package_with_invalid_workspace() -> Result<(), Error> { + let lockfile = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + // Test with invalid workspace + let result = lockfile.resolve_package("invalid/workspace", "lodash", "^4.17.21"); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::MissingWorkspace(_))); + + Ok(()) + } + + #[test] + fn test_global_change_detection() -> Result<(), Error> { + let lockfile1 = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + let lockfile2 = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + // Same lockfile should not show global change + assert!(!lockfile1.global_change(&lockfile2)); + + Ok(()) + } + + #[test] + fn test_all_dependencies_with_invalid_key() -> Result<(), Error> { + let lockfile = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + // Test with invalid package key + let result = lockfile.all_dependencies("invalid-package-key")?; + assert!(result.is_none()); + + Ok(()) + } + + #[test] + fn test_human_name_with_nonexistent_package() -> Result<(), Error> { + let lockfile = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + let package = Package { + key: "nonexistent-package".to_string(), + version: "1.0.0".to_string(), + }; + + let result = lockfile.human_name(&package); + assert!(result.is_none()); + + Ok(()) + } + + #[test] + fn test_patches_with_empty_lockfile() -> Result<(), Error> { + let lockfile = NpmLockfile::load(include_bytes!("../fixtures/npm-lock.json"))?; + + // NPM lockfile should have no patches by default + let patches = lockfile.patches()?; + assert!(patches.is_empty()); + + Ok(()) + } } diff --git a/crates/turborepo-lockfiles/src/pnpm/data.rs b/crates/turborepo-lockfiles/src/pnpm/data.rs index 8179845648fcd..5fefec84ac708 100644 --- a/crates/turborepo-lockfiles/src/pnpm/data.rs +++ b/crates/turborepo-lockfiles/src/pnpm/data.rs @@ -1483,4 +1483,85 @@ c: crate::Error::MissingWorkspace("apps/docs".to_string()).to_string() ); } + + #[test] + fn test_subgraph_with_empty_workspace_packages() { + let lockfile = PnpmLockfile::from_bytes(PNPM8).unwrap(); + + // Test subgraph with no workspace packages + let subgraph = lockfile.subgraph(&[], &["/is-odd@3.0.1".into()]).unwrap(); + let pnpm_subgraph = (subgraph.as_ref() as &dyn Any) + .downcast_ref::() + .unwrap(); + + // Should only contain root importer + assert_eq!(pnpm_subgraph.importers.len(), 1); + assert!(pnpm_subgraph.importers.contains_key(".")); + + // Should contain the requested package + assert!(pnpm_subgraph + .packages + .as_ref() + .unwrap() + .contains_key("/is-odd@3.0.1")); + } + + #[test] + fn test_subgraph_with_missing_package() { + let lockfile = PnpmLockfile::from_bytes(PNPM8).unwrap(); + + // Test subgraph with non-existent package + let result = lockfile.subgraph(&["packages/a".into()], &["nonexistent-package".into()]); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + crate::Error::MissingPackage(_) + )); + } + + #[test] + fn test_resolve_package_with_invalid_workspace() { + let lockfile = PnpmLockfile::from_bytes(PNPM8).unwrap(); + + // Test with invalid workspace + let result = lockfile.resolve_package("invalid/workspace", "is-odd", "^3.0.1"); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + crate::Error::MissingWorkspace(_) + )); + } + + #[test] + fn test_global_change_detection() { + let lockfile1 = PnpmLockfile::from_bytes(PNPM8).unwrap(); + let lockfile2 = PnpmLockfile::from_bytes(PNPM8_6).unwrap(); + + // Different lockfiles should show global change + assert!(lockfile1.global_change(&lockfile2)); + + // Same lockfile should not show global change + assert!(!lockfile1.global_change(&lockfile1)); + } + + #[test] + fn test_all_dependencies_with_invalid_key() { + let lockfile = PnpmLockfile::from_bytes(PNPM8).unwrap(); + + // Test with invalid package key + let result = lockfile.all_dependencies("invalid-package-key").unwrap(); + assert!(result.is_none()); + } + + #[test] + fn test_workspace_link_resolution() { + let lockfile = PnpmLockfile::from_bytes(PNPM8).unwrap(); + + // Test workspace link resolution - current implementation returns None + // for workspace links since they're not actual packages in the lockfile + let workspace_dep = lockfile + .resolve_package("packages/a", "c", "workspace:*") + .unwrap(); + assert!(workspace_dep.is_none()); + } }