Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions test/SeederApi.IntegrationTest/Cli/OrganizationArgsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,125 @@ public void ToOptions_NullOwnerEmail_StaysNull()
Assert.Null(options.OwnerEmail);
}

[Fact]
public void Validate_StripeBillingOnFreePlan_Throws()
{
var args = BaseArgs();
args.PlanType = "free";
args.StripeBilling = true;

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("Free plan", ex.Message);
}

[Fact]
public void Validate_StripeBillingOnPaidPlan_Passes()
{
var args = BaseArgs();
args.PlanType = "teams-monthly";
args.StripeBilling = true;

args.Validate();
}

[Theory]
[InlineData(true, null)]
[InlineData(false, 7)]
[InlineData(true, 7)]
public void Validate_TrialFlagsWithoutStripeBilling_Throws(bool skipTrial, int? trialDays)
{
var args = BaseArgs();
args.SkipTrial = skipTrial;
args.TrialDays = trialDays;

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("--stripe-billing", ex.Message);
}

[Fact]
public void Validate_SkipTrialAndTrialDaysTogether_Throws()
{
var args = BaseArgs();
args.StripeBilling = true;
args.SkipTrial = true;
args.TrialDays = 7;

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("mutually exclusive", ex.Message);
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(31)]
public void Validate_TrialDaysOutOfRange_Throws(int trialDays)
{
var args = BaseArgs();
args.StripeBilling = true;
args.TrialDays = trialDays;

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("between 1 and 30", ex.Message);
}

[Theory]
[InlineData(1)]
[InlineData(30)]
public void Validate_TrialDaysAtRangeBoundaries_Passes(int trialDays)
{
var args = BaseArgs();
args.StripeBilling = true;
args.TrialDays = trialDays;

args.Validate();
}

[Fact]
public void ToOptions_WithoutStripeBilling_LeavesBillingNull()
{
// The zero-Stripe-calls default: null is what every seeding path reads as "no billing".
Assert.Null(BaseArgs().ToOptions().StripeBilling);
}

[Fact]
public void ToOptions_StripeBillingDefaults_ThirtyDayTrial()
{
var args = BaseArgs();
args.StripeBilling = true;

var billing = args.ToOptions().StripeBilling;

Assert.NotNull(billing);
Assert.False(billing.SkipTrial);
Assert.Equal(30, billing.TrialDays);
}

[Fact]
public void ToOptions_StripeBillingWithTrialDays_MapsTheValue()
{
var args = BaseArgs();
args.StripeBilling = true;
args.TrialDays = 14;

var billing = args.ToOptions().StripeBilling;

Assert.NotNull(billing);
Assert.Equal(14, billing.TrialDays);
}

[Fact]
public void ToOptions_SkipTrial_MapsTheFlag()
{
var args = BaseArgs();
args.StripeBilling = true;
args.SkipTrial = true;

var billing = args.ToOptions().StripeBilling;

Assert.NotNull(billing);
Assert.True(billing.SkipTrial);
}

private static OrganizationArgs BaseArgs() => new()
{
Name = "Org",
Expand Down
76 changes: 76 additions & 0 deletions test/SeederApi.IntegrationTest/Cli/PresetArgsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,80 @@ public void Validate_OrgNameUnvalidated_Passes()
args = new PresetArgs { Name = "any", OrgName = "Anything goes πŸŽ‰" };
args.Validate();
}

[Theory]
[InlineData(true, null)]
[InlineData(false, 7)]
[InlineData(true, 7)]
public void Validate_TrialFlagsWithoutStripeBilling_Throws(bool skipTrial, int? trialDays)
{
var args = new PresetArgs { Name = "any", SkipTrial = skipTrial, TrialDays = trialDays };

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("--stripe-billing", ex.Message);
}

[Fact]
public void Validate_SkipTrialAndTrialDaysTogether_Throws()
{
var args = new PresetArgs { Name = "any", StripeBilling = true, SkipTrial = true, TrialDays = 7 };

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("mutually exclusive", ex.Message);
}

[Theory]
[InlineData(0)]
[InlineData(31)]
public void Validate_TrialDaysOutOfRange_Throws(int trialDays)
{
var args = new PresetArgs { Name = "any", StripeBilling = true, TrialDays = trialDays };

var ex = Assert.Throws<ArgumentException>(args.Validate);
Assert.Contains("between 1 and 30", ex.Message);
}

[Fact]
public void Validate_TrialFlagsWithListAndNoName_StillThrows()
{
// --list short-circuits the rest of Validate, so the trial checks have to precede it or
// `--list --trial-days 99` would pass silently.
var args = new PresetArgs { List = true, TrialDays = 99 };

Assert.Throws<ArgumentException>(args.Validate);
}

[Fact]
public void Validate_StripeBillingWithTrialDays_Passes()
{
var args = new PresetArgs { Name = "any", StripeBilling = true, TrialDays = 14 };
args.Validate();
}

[Fact]
public void ToStripeBillingOptions_WithoutFlag_IsNull()
{
Assert.Null(new PresetArgs { Name = "any" }.ToStripeBillingOptions());
}

[Fact]
public void ToStripeBillingOptions_WithFlag_MapsTrialConfiguration()
{
var options = new PresetArgs { Name = "any", StripeBilling = true, TrialDays = 14 }
.ToStripeBillingOptions();

Assert.NotNull(options);
Assert.False(options.SkipTrial);
Assert.Equal(14, options.TrialDays);
}

[Fact]
public void ToStripeBillingOptions_SkipTrial_DefaultsTrialDaysUnused()
{
var options = new PresetArgs { Name = "any", StripeBilling = true, SkipTrial = true }
.ToStripeBillingOptions();

Assert.NotNull(options);
Assert.True(options.SkipTrial);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ο»Ώusing AutoMapper;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.Repositories;
Expand Down Expand Up @@ -89,6 +90,37 @@ public async Task PostCommitStep_CannotContributeToResultAsync()
Assert.Equal(1, UsersInDb());
}

[Fact]
public async Task PostCommitStep_GatewayIdsOnOrganization_ReachTheResultAsync()
{
// The one documented exception to "a post-commit step cannot contribute to the result":
// RecipeExecutor re-projects the organization's gateway IDs after the post-commit loop.
var builder = _services.AddRecipe(_recipe);
builder.AddStep(_ => new RecordingStep("pre", _log, context =>
{
StageOwner(NewUser())(context);
context.Organization = new Organization { Id = CombGuid.Generate() };
}));
builder.AddAsyncStep(_ => new GatewayStampingPostCommitStep("cus_seeded", "sub_seeded"));

var result = await BuildExecutor().ExecuteAsync();

Assert.Equal("cus_seeded", result.GatewayCustomerId);
Assert.Equal("sub_seeded", result.GatewaySubscriptionId);
}

[Fact]
public async Task NoBillingStep_LeavesGatewayIdsNullAsync()
{
var builder = _services.AddRecipe(_recipe);
builder.AddStep(_ => new RecordingStep("pre", _log, StageOwner(NewUser())));

var result = await BuildExecutor().ExecuteAsync();

Assert.Null(result.GatewayCustomerId);
Assert.Null(result.GatewaySubscriptionId);
}

[Fact]
public async Task PostCommitStep_IsActuallyAwaitedAsync()
{
Expand Down Expand Up @@ -250,6 +282,19 @@ public Task ExecuteAsync(SeederContext context)
}
}

/// <summary>Stands in for <c>FinalizeOrganizationBillingStep</c>: stamps gateway IDs onto the committed org.</summary>
private sealed class GatewayStampingPostCommitStep(string customerId, string subscriptionId)
: IAsyncStep, IPostCommitStep
{
public Task ExecuteAsync(SeederContext context)
{
var organization = context.RequireOrganization();
organization.GatewayCustomerId = customerId;
organization.GatewaySubscriptionId = subscriptionId;
return Task.CompletedTask;
}
}

private sealed class StagingPostCommitStep(User user) : IAsyncStep, IPostCommitStep
{
public Task ExecuteAsync(SeederContext context)
Expand Down
Loading
Loading