1
0
mirror of https://github.com/bitwarden/server synced 2026-02-14 15:33:35 +00:00

Enhance seeder with additional cipher types and architectural refactorings (#6935)

This commit is contained in:
Mick Letofsky
2026-02-04 19:27:09 +01:00
committed by GitHub
parent 26b62bc766
commit 4eb9c4cf3c
67 changed files with 2968 additions and 959 deletions

View File

@@ -8,6 +8,7 @@ using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Core.Entities;
using Bit.Seeder.Recipes;
using Bit.Seeder.Services;
using Microsoft.AspNetCore.Identity;
using Xunit;
using Xunit.Abstractions;
@@ -31,7 +32,8 @@ public class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = new NoOpManglerService();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);

View File

@@ -10,6 +10,7 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Seeder.Recipes;
using Bit.Seeder.Services;
using Microsoft.AspNetCore.Identity;
using Xunit;
using Xunit.Abstractions;
@@ -33,7 +34,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
@@ -71,7 +73,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
@@ -107,7 +110,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var groupsSeeder = new GroupsRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
@@ -141,7 +145,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
@@ -176,7 +181,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
@@ -226,7 +232,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
@@ -268,7 +275,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
@@ -314,7 +322,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
@@ -360,7 +369,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domainSeeder = new OrganizationDomainRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
@@ -407,7 +417,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
@@ -459,7 +470,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
@@ -498,7 +510,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domainSeeder = new OrganizationDomainRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
@@ -541,7 +554,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
@@ -591,7 +605,8 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = factory.GetService<IManglerService>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(

View File

@@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Tokens;
using Bit.Seeder.Recipes;
using Bit.Seeder.Services;
using Microsoft.AspNetCore.Identity;
using Xunit;
using Xunit.Abstractions;
@@ -34,7 +35,8 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = new NoOpManglerService();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
@@ -84,7 +86,8 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
var db = factory.GetDatabaseContext();
var mapper = factory.GetService<IMapper>();
var passwordHasher = factory.GetService<IPasswordHasher<User>>();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher);
var manglerService = new NoOpManglerService();
var orgSeeder = new OrganizationWithUsersRecipe(db, mapper, passwordHasher, manglerService);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);

View File

@@ -7,6 +7,7 @@ using Bit.Core.Platform.PushRegistration.Internal;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
@@ -190,6 +191,9 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
TestDatabase.Migrate(services);
}
// Register NoOpManglerService for test data seeding (no mangling in tests)
services.TryAddSingleton<IManglerService, NoOpManglerService>();
// QUESTION: The normal licensing service should run fine on developer machines but not in CI
// should we have a fork here to leave the normal service for developers?
// TODO: Eventually add the license file to CI

View File

@@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
<ProjectReference Include="..\..\util\Migrator\Migrator.csproj" />
<ProjectReference Include="..\..\util\Seeder\Seeder.csproj" />
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,175 @@
using Bit.Seeder.Data.Distributions;
using Xunit;
namespace Bit.SeederApi.IntegrationTest;
public class DistributionTests
{
[Fact]
public void Constructor_PercentagesSumToOne_Succeeds()
{
var distribution = new Distribution<string>(
("A", 0.50),
("B", 0.30),
("C", 0.20)
);
Assert.NotNull(distribution);
}
[Fact]
public void Constructor_PercentagesDoNotSumToOne_ThrowsArgumentException()
{
var exception = Assert.Throws<ArgumentException>(() => new Distribution<int>(
(1, 0.50),
(2, 0.40)
));
Assert.Contains("must sum to 1.0", exception.Message);
}
[Fact]
public void Constructor_PercentagesExceedOne_ThrowsArgumentException()
{
var exception = Assert.Throws<ArgumentException>(() => new Distribution<string>(
("X", 0.60),
("Y", 0.60)
));
Assert.Contains("must sum to 1.0", exception.Message);
}
[Fact]
public void Constructor_WithinToleranceOf001_Succeeds()
{
var distribution = new Distribution<string>(
("A", 0.333),
("B", 0.333),
("C", 0.333)
);
Assert.NotNull(distribution);
}
[Fact]
public void Select_ReturnsCorrectBuckets_ForEvenSplit()
{
var distribution = new Distribution<string>(
("A", 0.50),
("B", 0.50)
);
Assert.Equal("A", distribution.Select(0, 100));
Assert.Equal("A", distribution.Select(49, 100));
Assert.Equal("B", distribution.Select(50, 100));
Assert.Equal("B", distribution.Select(99, 100));
}
[Fact]
public void Select_ReturnsCorrectBuckets_ForThreeWaySplit()
{
var distribution = new Distribution<int>(
(1, 0.60),
(2, 0.30),
(3, 0.10)
);
Assert.Equal(1, distribution.Select(0, 100));
Assert.Equal(1, distribution.Select(59, 100));
Assert.Equal(2, distribution.Select(60, 100));
Assert.Equal(2, distribution.Select(89, 100));
Assert.Equal(3, distribution.Select(90, 100));
Assert.Equal(3, distribution.Select(99, 100));
}
[Fact]
public void Select_IndexBeyondTotal_ReturnsLastBucket()
{
var distribution = new Distribution<string>(
("A", 0.50),
("B", 0.50)
);
Assert.Equal("B", distribution.Select(150, 100));
}
[Fact]
public void Select_SmallTotal_HandlesRoundingGracefully()
{
var distribution = new Distribution<string>(
("A", 0.33),
("B", 0.33),
("C", 0.34)
);
Assert.Equal("A", distribution.Select(0, 10));
Assert.Equal("A", distribution.Select(2, 10));
Assert.Equal("B", distribution.Select(3, 10));
Assert.Equal("C", distribution.Select(9, 10));
}
[Fact]
public void GetCounts_ReturnsCorrectCounts_ForEvenSplit()
{
var distribution = new Distribution<string>(
("X", 0.50),
("Y", 0.50)
);
var counts = distribution.GetCounts(100).ToList();
Assert.Equal(2, counts.Count);
Assert.Equal(("X", 50), counts[0]);
Assert.Equal(("Y", 50), counts[1]);
}
[Fact]
public void GetCounts_LastBucketReceivesRemainder()
{
var distribution = new Distribution<string>(
("A", 0.33),
("B", 0.33),
("C", 0.34)
);
var counts = distribution.GetCounts(100).ToList();
Assert.Equal(3, counts.Count);
Assert.Equal(33, counts[0].Count);
Assert.Equal(33, counts[1].Count);
Assert.Equal(34, counts[2].Count);
}
[Fact]
public void GetCounts_TotalCountsMatchInput()
{
var distribution = new Distribution<int>(
(1, 0.25),
(2, 0.25),
(3, 0.25),
(4, 0.25)
);
var counts = distribution.GetCounts(1000).ToList();
var total = counts.Sum(c => c.Count);
Assert.Equal(1000, total);
}
[Fact]
public void Select_IsDeterministic_SameInputSameOutput()
{
var distribution = new Distribution<string>(
("Alpha", 0.40),
("Beta", 0.35),
("Gamma", 0.25)
);
for (int i = 0; i < 100; i++)
{
var first = distribution.Select(i, 100);
var second = distribution.Select(i, 100);
Assert.Equal(first, second);
}
}
}

View File

@@ -0,0 +1,262 @@
using Bit.Seeder.Data;
using Bit.Seeder.Data.Distributions;
using Bit.Seeder.Data.Enums;
using Bit.Seeder.Options;
using Xunit;
namespace Bit.SeederApi.IntegrationTest;
public class GeneratorContextTests
{
[Fact]
public void FromOptions_SameDomain_ProducesSameSeed()
{
var options1 = CreateOptions("acme.com", ciphers: 100);
var options2 = CreateOptions("acme.com", ciphers: 100);
var ctx1 = GeneratorContext.FromOptions(options1);
var ctx2 = GeneratorContext.FromOptions(options2);
Assert.Equal(ctx1.Seed, ctx2.Seed);
}
[Fact]
public void FromOptions_DifferentDomains_ProduceDifferentSeeds()
{
var ctx1 = GeneratorContext.FromOptions(CreateOptions("acme.com"));
var ctx2 = GeneratorContext.FromOptions(CreateOptions("contoso.com"));
Assert.NotEqual(ctx1.Seed, ctx2.Seed);
}
[Fact]
public void FromOptions_ExplicitSeed_OverridesDomainHash()
{
var options = new OrganizationVaultOptions
{
Name = "Test Org",
Domain = "example.com",
Users = 10,
Ciphers = 100,
Seed = 42
};
var ctx = GeneratorContext.FromOptions(options);
Assert.Equal(42, ctx.Seed);
}
[Fact]
public void Username_SameSeed_ProducesSameOutput()
{
var options = CreateOptions("test.com", ciphers: 100);
var ctx1 = GeneratorContext.FromOptions(options);
var ctx2 = GeneratorContext.FromOptions(options);
for (int i = 0; i < 50; i++)
{
var username1 = ctx1.Username.GenerateByIndex(i, totalHint: 100, domain: "test.com");
var username2 = ctx2.Username.GenerateByIndex(i, totalHint: 100, domain: "test.com");
Assert.Equal(username1, username2);
}
}
[Fact]
public void Username_DifferentSeeds_ProducesDifferentOutput()
{
var ctx1 = GeneratorContext.FromOptions(CreateOptions("alpha.com"));
var ctx2 = GeneratorContext.FromOptions(CreateOptions("beta.com"));
var username1 = ctx1.Username.GenerateByIndex(0, domain: "alpha.com");
var username2 = ctx2.Username.GenerateByIndex(0, domain: "beta.com");
Assert.NotEqual(username1, username2);
}
[Fact]
public void Folder_SameSeed_ProducesSameOutput()
{
var options = CreateOptions("test.com");
var ctx1 = GeneratorContext.FromOptions(options);
var ctx2 = GeneratorContext.FromOptions(options);
for (int i = 0; i < 20; i++)
{
var folder1 = ctx1.Folder.GetFolderName(i);
var folder2 = ctx2.Folder.GetFolderName(i);
Assert.Equal(folder1, folder2);
}
}
[Fact]
public void Card_SameSeed_ProducesSameOutput()
{
var options = CreateOptions("test.com");
var ctx1 = GeneratorContext.FromOptions(options);
var ctx2 = GeneratorContext.FromOptions(options);
for (int i = 0; i < 20; i++)
{
var card1 = ctx1.Card.GenerateByIndex(i);
var card2 = ctx2.Card.GenerateByIndex(i);
Assert.Equal(card1.CardholderName, card2.CardholderName);
Assert.Equal(card1.Number, card2.Number);
Assert.Equal(card1.ExpMonth, card2.ExpMonth);
Assert.Equal(card1.ExpYear, card2.ExpYear);
Assert.Equal(card1.Code, card2.Code);
}
}
[Fact]
public void Identity_SameSeed_ProducesSameOutput()
{
var options = CreateOptions("test.com");
var ctx1 = GeneratorContext.FromOptions(options);
var ctx2 = GeneratorContext.FromOptions(options);
for (int i = 0; i < 20; i++)
{
var identity1 = ctx1.Identity.GenerateByIndex(i);
var identity2 = ctx2.Identity.GenerateByIndex(i);
Assert.Equal(identity1.FirstName, identity2.FirstName);
Assert.Equal(identity1.LastName, identity2.LastName);
Assert.Equal(identity1.Email, identity2.Email);
}
}
/// <summary>
/// Limited to 5 iterations to avoid a Bogus.Password() infinite loop bug
/// that occurs with certain seed/index combinations in WiFi/Database note categories.
/// The workaround is a known test workaround that doesn't affect production code.
/// </summary>
[Fact]
public void SecureNote_SameSeed_ProducesSameOutput()
{
var options = CreateOptions("test.com");
var ctx1 = GeneratorContext.FromOptions(options);
var ctx2 = GeneratorContext.FromOptions(options);
for (var i = 0; i < 5; i++)
{
var (title1, content1) = ctx1.SecureNote.GenerateByIndex(i);
var (title2, content2) = ctx2.SecureNote.GenerateByIndex(i);
Assert.Equal(title1, title2);
Assert.Equal(content1, content2);
}
}
[Fact]
public void CipherCount_ReflectsOptionsValue()
{
var options = CreateOptions("test.com", ciphers: 500);
var ctx = GeneratorContext.FromOptions(options);
Assert.Equal(500, ctx.CipherCount);
}
[Fact]
public void Username_WithCorporatePattern_AppliesCorrectFormat()
{
var options = new OrganizationVaultOptions
{
Name = "Test Org",
Domain = "corp.com",
Users = 10,
Ciphers = 100,
UsernamePattern = UsernamePatternType.FDotLast,
UsernameDistribution = new Distribution<UsernameCategory>(
(UsernameCategory.CorporateEmail, 1.0)
)
};
var ctx = GeneratorContext.FromOptions(options);
var username = ctx.Username.GenerateByIndex(0, domain: "corp.com");
Assert.Matches(@"^[a-z]\.[a-z]+@corp\.com$", username);
}
[Fact]
public void Username_WithRegion_ProducesCulturallyAppropriateNames()
{
var europeOptions = new OrganizationVaultOptions
{
Name = "Euro Corp",
Domain = "euro.com",
Users = 10,
Ciphers = 100,
Region = GeographicRegion.Europe,
UsernameDistribution = new Distribution<UsernameCategory>(
(UsernameCategory.CorporateEmail, 1.0)
)
};
var ctx = GeneratorContext.FromOptions(europeOptions);
var username = ctx.Username.GenerateByIndex(0, domain: "euro.com");
Assert.Contains("@euro.com", username);
Assert.Matches(@"^[\p{L}]+\.[\p{L}]+@euro\.com$", username);
}
[Fact]
public void Generators_AreLazilyInitialized()
{
var options = CreateOptions("test.com");
var ctx = GeneratorContext.FromOptions(options);
_ = ctx.Seed;
_ = ctx.Username.GenerateByIndex(0);
Assert.NotNull(ctx.Username);
Assert.NotNull(ctx.Folder);
Assert.NotNull(ctx.Card);
Assert.NotNull(ctx.Identity);
}
[Fact]
public void AllGenerators_ProduceDifferentOutputForDifferentIndices()
{
var ctx = GeneratorContext.FromOptions(CreateOptions("test.com", ciphers: 100));
var usernames = Enumerable.Range(0, 50)
.Select(i => ctx.Username.GenerateByIndex(i, domain: "test.com"))
.ToHashSet();
Assert.True(usernames.Count > 40, "Should generate mostly unique usernames");
var folders = Enumerable.Range(0, 50)
.Select(i => ctx.Folder.GetFolderName(i))
.ToHashSet();
Assert.True(folders.Count > 30, "Should generate diverse folder names");
var cards = Enumerable.Range(0, 50)
.Select(i => ctx.Card.GenerateByIndex(i).Number)
.ToHashSet();
Assert.True(cards.Count > 40, "Should generate mostly unique card numbers");
var identities = Enumerable.Range(0, 50)
.Select(i => ctx.Identity.GenerateByIndex(i).Email)
.ToHashSet();
Assert.True(identities.Count > 40, "Should generate mostly unique identity emails");
}
private static OrganizationVaultOptions CreateOptions(string domain, int ciphers = 100)
{
return new OrganizationVaultOptions
{
Name = "Test Org",
Domain = domain,
Users = 10,
Ciphers = ciphers
};
}
}

View File

@@ -110,7 +110,7 @@ public class RustSdkCipherTests
},
Fields =
[
new FieldViewDto { Name = "API Key", Value = "sk-secret-api-key-12345", Type = 1 },
new FieldViewDto { Name = "API Key", Value = "sk_test_FAKE_api_key_12345", Type = 1 },
new FieldViewDto { Name = "Client ID", Value = "client-id-xyz", Type = 0 }
]
};
@@ -128,7 +128,7 @@ public class RustSdkCipherTests
Assert.NotNull(decrypted?.Fields);
Assert.Equal(2, decrypted.Fields.Count);
Assert.Equal("API Key", decrypted.Fields[0].Name);
Assert.Equal("sk-secret-api-key-12345", decrypted.Fields[0].Value);
Assert.Equal("sk_test_FAKE_api_key_12345", decrypted.Fields[0].Value);
}
[Fact]
@@ -138,10 +138,10 @@ public class RustSdkCipherTests
var orgId = Guid.NewGuid();
// Create cipher using the seeder
var cipher = CipherSeeder.CreateOrganizationLoginCipher(
orgId,
var cipher = LoginCipherSeeder.Create(
orgKeys.Key,
name: "GitHub Account",
organizationId: orgId,
username: "developer@example.com",
password: "SecureP@ss123!",
uri: "https://github.com",
@@ -175,16 +175,16 @@ public class RustSdkCipherTests
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var cipher = CipherSeeder.CreateOrganizationLoginCipherWithFields(
Guid.NewGuid(),
var cipher = LoginCipherSeeder.Create(
orgKeys.Key,
name: "API Service",
organizationId: Guid.NewGuid(),
username: "service@example.com",
password: "SvcP@ss!",
uri: "https://api.example.com",
fields: [
("API Key", "sk-live-abc123", 1), // Hidden field
("Environment", "production", 0) // Text field
("API Key", "sk_test_FAKE_abc123", 1),
("Environment", "production", 0)
]);
var loginData = JsonSerializer.Deserialize<CipherLoginData>(cipher.Data);
@@ -204,7 +204,7 @@ public class RustSdkCipherTests
Assert.Equal(Core.Vault.Enums.FieldType.Text, fields[1].Type);
Assert.DoesNotContain("API Key", cipher.Data);
Assert.DoesNotContain("sk-live-abc123", cipher.Data);
Assert.DoesNotContain("sk_test_FAKE_abc123", cipher.Data);
}
private static CipherViewDto CreateTestLoginCipher()
@@ -223,4 +223,268 @@ public class RustSdkCipherTests
};
}
[Fact]
public void EncryptDecrypt_CardCipher_RoundtripPreservesPlaintext()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var originalCipher = new CipherViewDto
{
Name = "My Visa Card",
Notes = "Primary card for online purchases",
Type = CipherTypes.Card,
Card = new CardViewDto
{
CardholderName = "John Doe",
Brand = "Visa",
Number = "4111111111111111",
ExpMonth = "12",
ExpYear = "2028",
Code = "123"
}
};
var originalJson = JsonSerializer.Serialize(originalCipher, SdkJsonOptions);
var encryptedJson = RustSdkService.EncryptCipher(originalJson, orgKeys.Key);
Assert.DoesNotContain("\"error\"", encryptedJson);
Assert.DoesNotContain("4111111111111111", encryptedJson);
Assert.DoesNotContain("John Doe", encryptedJson);
var decryptedJson = RustSdkService.DecryptCipher(encryptedJson, orgKeys.Key);
var decrypted = JsonSerializer.Deserialize<CipherViewDto>(decryptedJson, SdkJsonOptions);
Assert.NotNull(decrypted?.Card);
Assert.Equal("4111111111111111", decrypted.Card.Number);
Assert.Equal("John Doe", decrypted.Card.CardholderName);
Assert.Equal("123", decrypted.Card.Code);
}
[Fact]
public void CipherSeeder_CardCipher_ProducesServerCompatibleFormat()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();
var card = new CardViewDto
{
CardholderName = "Jane Smith",
Brand = "Mastercard",
Number = "5500000000000004",
ExpMonth = "06",
ExpYear = "2027",
Code = "456"
};
var cipher = CardCipherSeeder.Create(orgKeys.Key, name: "Business Card", card: card, organizationId: orgId, notes: "Company expenses");
Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.Card, cipher.Type);
var cardData = JsonSerializer.Deserialize<CipherCardData>(cipher.Data);
Assert.NotNull(cardData);
var encStringPrefix = "2.";
Assert.StartsWith(encStringPrefix, cardData.Name);
Assert.StartsWith(encStringPrefix, cardData.CardholderName);
Assert.StartsWith(encStringPrefix, cardData.Number);
Assert.StartsWith(encStringPrefix, cardData.Code);
Assert.DoesNotContain("5500000000000004", cipher.Data);
Assert.DoesNotContain("Jane Smith", cipher.Data);
}
[Fact]
public void EncryptDecrypt_IdentityCipher_RoundtripPreservesPlaintext()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var originalCipher = new CipherViewDto
{
Name = "Personal Identity",
Type = CipherTypes.Identity,
Identity = new IdentityViewDto
{
Title = "Mr",
FirstName = "John",
MiddleName = "Robert",
LastName = "Doe",
Email = "john.doe@example.com",
Phone = "+1-555-123-4567",
SSN = "123-45-6789",
Address1 = "123 Main Street",
City = "Anytown",
State = "CA",
PostalCode = "90210",
Country = "US"
}
};
var originalJson = JsonSerializer.Serialize(originalCipher, SdkJsonOptions);
var encryptedJson = RustSdkService.EncryptCipher(originalJson, orgKeys.Key);
Assert.DoesNotContain("\"error\"", encryptedJson);
Assert.DoesNotContain("123-45-6789", encryptedJson);
Assert.DoesNotContain("john.doe@example.com", encryptedJson);
var decryptedJson = RustSdkService.DecryptCipher(encryptedJson, orgKeys.Key);
var decrypted = JsonSerializer.Deserialize<CipherViewDto>(decryptedJson, SdkJsonOptions);
Assert.NotNull(decrypted?.Identity);
Assert.Equal("John", decrypted.Identity.FirstName);
Assert.Equal("123-45-6789", decrypted.Identity.SSN);
Assert.Equal("john.doe@example.com", decrypted.Identity.Email);
}
[Fact]
public void CipherSeeder_IdentityCipher_ProducesServerCompatibleFormat()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();
var identity = new IdentityViewDto
{
Title = "Dr",
FirstName = "Alice",
LastName = "Johnson",
Email = "alice@company.com",
SSN = "987-65-4321",
PassportNumber = "X12345678"
};
var cipher = IdentityCipherSeeder.Create(orgKeys.Key, name: "Dr. Alice Johnson", identity: identity, organizationId: orgId);
Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.Identity, cipher.Type);
var identityData = JsonSerializer.Deserialize<CipherIdentityData>(cipher.Data);
Assert.NotNull(identityData);
var encStringPrefix = "2.";
Assert.StartsWith(encStringPrefix, identityData.Name);
Assert.StartsWith(encStringPrefix, identityData.FirstName);
Assert.StartsWith(encStringPrefix, identityData.SSN);
Assert.DoesNotContain("987-65-4321", cipher.Data);
Assert.DoesNotContain("Alice", cipher.Data);
}
[Fact]
public void EncryptDecrypt_SecureNoteCipher_RoundtripPreservesPlaintext()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var originalCipher = new CipherViewDto
{
Name = "API Secrets",
Notes = "sk_test_FAKE_abc123xyz789key",
Type = CipherTypes.SecureNote,
SecureNote = new SecureNoteViewDto { Type = 0 }
};
var originalJson = JsonSerializer.Serialize(originalCipher, SdkJsonOptions);
var encryptedJson = RustSdkService.EncryptCipher(originalJson, orgKeys.Key);
Assert.DoesNotContain("\"error\"", encryptedJson);
Assert.DoesNotContain("sk_test_FAKE_abc123xyz789key", encryptedJson);
var decryptedJson = RustSdkService.DecryptCipher(encryptedJson, orgKeys.Key);
var decrypted = JsonSerializer.Deserialize<CipherViewDto>(decryptedJson, SdkJsonOptions);
Assert.NotNull(decrypted);
Assert.Equal("API Secrets", decrypted.Name);
Assert.Equal("sk_test_FAKE_abc123xyz789key", decrypted.Notes);
}
[Fact]
public void CipherSeeder_SecureNoteCipher_ProducesServerCompatibleFormat()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();
var cipher = SecureNoteCipherSeeder.Create(
orgKeys.Key,
name: "Production Secrets",
organizationId: orgId,
notes: "DATABASE_URL=postgres://user:FAKE_secret@db.example.com/prod");
Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.SecureNote, cipher.Type);
var noteData = JsonSerializer.Deserialize<CipherSecureNoteData>(cipher.Data);
Assert.NotNull(noteData);
Assert.Equal(Core.Vault.Enums.SecureNoteType.Generic, noteData.Type);
var encStringPrefix = "2.";
Assert.StartsWith(encStringPrefix, noteData.Name);
Assert.StartsWith(encStringPrefix, noteData.Notes);
Assert.DoesNotContain("postgres://", cipher.Data);
Assert.DoesNotContain("secret", cipher.Data);
}
[Fact]
public void EncryptDecrypt_SshKeyCipher_RoundtripPreservesPlaintext()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var originalCipher = new CipherViewDto
{
Name = "Dev Server Key",
Type = CipherTypes.SshKey,
SshKey = new SshKeyViewDto
{
PrivateKey = "-----BEGIN FAKE RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...\n-----END FAKE RSA PRIVATE KEY-----",
PublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... user@host",
Fingerprint = "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8"
}
};
var originalJson = JsonSerializer.Serialize(originalCipher, SdkJsonOptions);
var encryptedJson = RustSdkService.EncryptCipher(originalJson, orgKeys.Key);
Assert.DoesNotContain("\"error\"", encryptedJson);
Assert.DoesNotContain("BEGIN FAKE RSA PRIVATE KEY", encryptedJson);
Assert.DoesNotContain("ssh-rsa AAAAB3", encryptedJson);
var decryptedJson = RustSdkService.DecryptCipher(encryptedJson, orgKeys.Key);
var decrypted = JsonSerializer.Deserialize<CipherViewDto>(decryptedJson, SdkJsonOptions);
Assert.NotNull(decrypted?.SshKey);
Assert.Contains("BEGIN FAKE RSA PRIVATE KEY", decrypted.SshKey.PrivateKey);
Assert.StartsWith("ssh-rsa", decrypted.SshKey.PublicKey);
Assert.StartsWith("SHA256:", decrypted.SshKey.Fingerprint);
}
[Fact]
public void CipherSeeder_SshKeyCipher_ProducesServerCompatibleFormat()
{
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var orgId = Guid.NewGuid();
var sshKey = new SshKeyViewDto
{
PrivateKey = "-----BEGIN FAKE OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA...\n-----END FAKE OPENSSH PRIVATE KEY-----",
PublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample test@machine",
Fingerprint = "SHA256:examplefingerprint123"
};
var cipher = SshKeyCipherSeeder.Create(orgKeys.Key, name: "Production Deploy Key", sshKey: sshKey, organizationId: orgId);
Assert.Equal(orgId, cipher.OrganizationId);
Assert.Equal(Core.Vault.Enums.CipherType.SSHKey, cipher.Type);
var sshData = JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data);
Assert.NotNull(sshData);
var encStringPrefix = "2.";
Assert.StartsWith(encStringPrefix, sshData.Name);
Assert.StartsWith(encStringPrefix, sshData.PrivateKey);
Assert.StartsWith(encStringPrefix, sshData.PublicKey);
Assert.StartsWith(encStringPrefix, sshData.KeyFingerprint);
Assert.DoesNotContain("BEGIN FAKE OPENSSH PRIVATE KEY", cipher.Data);
Assert.DoesNotContain("ssh-ed25519", cipher.Data);
}
}