using Bit.Core.Billing.Enums; using Bit.Core.Vault.Enums; using Bit.Seeder.Data.Distributions; using Bit.Seeder.Data.Enums; using Bit.Seeder.Steps; namespace Bit.Seeder.Pipeline; /// /// Step registration extension methods for . /// Each method validates constraints, sets validation flags, and registers the step via DI. /// public static class RecipeBuilderExtensions { /// /// Use an organization from embedded fixtures with optional plan/seats overrides from the preset. /// /// The recipe builder /// Organization fixture name without extension /// Optional plan type override (from preset) /// Optional seats override (from preset) /// The builder for fluent chaining public static RecipeBuilder UseOrganization(this RecipeBuilder builder, string fixture, string? planType = null, int? seats = null) { builder.HasOrg = true; builder.AddStep(_ => CreateOrganizationStep.FromFixture(fixture, planType, seats)); return builder; } /// /// Create an organization inline with specified parameters. /// /// The recipe builder /// Organization display name /// Organization domain (used for email generation) /// Number of user seats /// Billing plan type (defaults to EnterpriseAnnually) /// The builder for fluent chaining public static RecipeBuilder CreateOrganization(this RecipeBuilder builder, string name, string domain, int? seats = null, PlanType planType = PlanType.EnterpriseAnnually) { builder.HasOrg = true; builder.AddStep(_ => CreateOrganizationStep.FromParams(name, domain, seats, planType)); return builder; } /// /// Add an organization owner user with admin privileges. /// /// The recipe builder /// The builder for fluent chaining public static RecipeBuilder AddOwner(this RecipeBuilder builder) { builder.HasOwner = true; builder.AddStep(_ => new CreateOwnerStep()); return builder; } /// /// Initialize seeded random generator for reproducible test data. /// /// The recipe builder /// Organization domain (used for seeding randomness) /// Optional explicit seed. If null, domain hash is used. /// The builder for fluent chaining public static RecipeBuilder WithGenerator(this RecipeBuilder builder, string domain, int? seed = null) { builder.HasGenerator = true; builder.AddStep(_ => InitGeneratorStep.FromDomain(domain, seed)); return builder; } /// /// Use a roster from embedded fixtures (users, groups, collections). /// /// The recipe builder /// Roster fixture name without extension /// The builder for fluent chaining /// Thrown when AddUsers() was already called public static RecipeBuilder UseRoster(this RecipeBuilder builder, string fixture) { if (builder.HasGeneratedUsers) { throw new InvalidOperationException( "Cannot call UseRoster() after AddUsers(). Choose one user source."); } builder.HasRosterUsers = true; builder.AddStep(_ => new CreateRosterStep(fixture)); return builder; } /// /// Generate users with seeded random data. /// /// The recipe builder /// Number of users to generate /// If true, includes revoked/invited users; if false, all confirmed /// The builder for fluent chaining /// Thrown when UseRoster() was already called public static RecipeBuilder AddUsers(this RecipeBuilder builder, int count, bool realisticStatusMix = false) { if (builder.HasRosterUsers) { throw new InvalidOperationException( "Cannot call AddUsers() after UseRoster(). Choose one user source."); } builder.HasGeneratedUsers = true; builder.AddStep(_ => new CreateUsersStep(count, realisticStatusMix)); return builder; } /// /// Generate groups with random members from existing users. /// /// The recipe builder /// Number of groups to generate /// The builder for fluent chaining /// Thrown when no users exist public static RecipeBuilder AddGroups(this RecipeBuilder builder, int count) { if (!builder.HasRosterUsers && !builder.HasGeneratedUsers) { throw new InvalidOperationException( "Groups require users. Call UseRoster() or AddUsers() first."); } builder.AddStep(_ => new CreateGroupsStep(count)); return builder; } /// /// Generate collections with random assignments. /// /// The recipe builder /// Number of collections to generate /// The builder for fluent chaining /// Thrown when no users exist public static RecipeBuilder AddCollections(this RecipeBuilder builder, int count) { if (!builder.HasRosterUsers && !builder.HasGeneratedUsers) { throw new InvalidOperationException( "Collections require users. Call UseRoster() or AddUsers() first."); } builder.AddStep(_ => CreateCollectionsStep.FromCount(count)); return builder; } /// /// Generate collections based on organizational structure model. /// /// The recipe builder /// Organizational structure (Traditional, Spotify, Modern) /// The builder for fluent chaining /// Thrown when no users exist public static RecipeBuilder AddCollections(this RecipeBuilder builder, OrgStructureModel structure) { if (!builder.HasRosterUsers && !builder.HasGeneratedUsers) { throw new InvalidOperationException( "Collections require users. Call UseRoster() or AddUsers() first."); } builder.AddStep(_ => CreateCollectionsStep.FromStructure(structure)); return builder; } /// /// Generate folders for each user using a realistic distribution. /// public static RecipeBuilder AddFolders(this RecipeBuilder builder) { if (!builder.HasRosterUsers && !builder.HasGeneratedUsers) { throw new InvalidOperationException( "Folders require users. Call UseRoster() or AddUsers() first."); } builder.HasFolders = true; builder.AddStep(_ => new GenerateFoldersStep()); return builder; } /// /// Use ciphers from embedded fixtures. /// /// The recipe builder /// Cipher fixture name without extension /// The builder for fluent chaining /// Thrown when AddCiphers() was already called public static RecipeBuilder UseCiphers(this RecipeBuilder builder, string fixture) { if (builder.HasGeneratedCiphers) { throw new InvalidOperationException( "Cannot call UseCiphers() after AddCiphers(). Choose one cipher source."); } builder.HasFixtureCiphers = true; builder.AddStep(_ => new CreateCiphersStep(fixture)); return builder; } /// /// Generate ciphers with configurable type and password strength distributions. /// /// The recipe builder /// Number of ciphers to generate /// Distribution of cipher types. Uses realistic defaults if null. /// Distribution of password strengths. Uses realistic defaults if null. /// When true, assigns ciphers to user folders round-robin. /// The builder for fluent chaining /// Thrown when UseCiphers() was already called public static RecipeBuilder AddCiphers( this RecipeBuilder builder, int count, Distribution? typeDist = null, Distribution? pwDist = null, bool assignFolders = false) { if (builder.HasFixtureCiphers) { throw new InvalidOperationException( "Cannot call AddCiphers() after UseCiphers(). Choose one cipher source."); } builder.HasGeneratedCiphers = true; if (assignFolders) { builder.HasCipherFolderAssignment = true; } builder.AddStep(_ => new GenerateCiphersStep(count, typeDist, pwDist, assignFolders)); return builder; } /// /// Generate personal ciphers for each user, encrypted with their individual symmetric key. /// /// The recipe builder /// Number of personal ciphers per user /// Distribution of cipher types. Uses realistic defaults if null. /// Distribution of password strengths. Uses realistic defaults if null. /// The builder for fluent chaining /// Thrown when no users exist public static RecipeBuilder AddPersonalCiphers( this RecipeBuilder builder, int countPerUser, Distribution? typeDist = null, Distribution? pwDist = null) { if (!builder.HasRosterUsers && !builder.HasGeneratedUsers) { throw new InvalidOperationException( "Personal ciphers require users. Call UseRoster() or AddUsers() first."); } builder.HasPersonalCiphers = true; builder.AddStep(_ => new GeneratePersonalCiphersStep(countPerUser, typeDist, pwDist)); return builder; } /// /// Validates the builder state to ensure all required steps are present and dependencies are met. /// /// The recipe builder /// The builder for fluent chaining /// Thrown when required steps missing or dependencies violated public static RecipeBuilder Validate(this RecipeBuilder builder) { if (!builder.HasOrg) { throw new InvalidOperationException( "Organization is required. Call UseOrganization() or CreateOrganization()."); } if (!builder.HasOwner) { throw new InvalidOperationException( "Owner is required. Call AddOwner()."); } if (builder.HasGeneratedCiphers && !builder.HasGenerator) { throw new InvalidOperationException( "Generated ciphers require a generator. Call WithGenerator() first."); } if (builder.HasPersonalCiphers && !builder.HasGenerator) { throw new InvalidOperationException( "Personal ciphers require a generator. Call WithGenerator() first."); } if (builder.HasFolders && !builder.HasGenerator) { throw new InvalidOperationException( "Folders require a generator. Call WithGenerator() first."); } if (builder.HasCipherFolderAssignment && !builder.HasFolders) { throw new InvalidOperationException( "Cipher folder assignment requires folders. Set 'folders: true' or call AddFolders() first."); } return builder; } }