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;
}
}