diff --git a/TestResumeBuilder/TestBase.cs b/TestResumeBuilder/TestBase.cs index 2a5ea91..2d10e77 100644 --- a/TestResumeBuilder/TestBase.cs +++ b/TestResumeBuilder/TestBase.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore; using resume_builder; using resume_builder.models; using Spectre.Console; @@ -19,8 +20,7 @@ protected TestBase() //given TestApp = new CommandAppTester(new FakeTypeRegistrar()); - TestApp.Configure(c => - { + TestApp.Configure(c => { Program.AppConfiguration(c); // c.ConfigureConsole(TestConsole); //? this is what spectre does inside of Run() but only if the config is null but either way it didn't work for me }); @@ -31,6 +31,7 @@ protected TestBase() TestConsole = new TestConsole(); AnsiConsole.Console = TestConsole; TestDb = new ResumeContext(); + TestDb.Database.Migrate(); } public async ValueTask DisposeAsync() diff --git a/TestResumeBuilder/TestResumeBuilder.csproj b/TestResumeBuilder/TestResumeBuilder.csproj index fb514eb..cce3540 100644 --- a/TestResumeBuilder/TestResumeBuilder.csproj +++ b/TestResumeBuilder/TestResumeBuilder.csproj @@ -11,6 +11,8 @@ + + diff --git a/TestResumeBuilder/commands/InitTest.cs b/TestResumeBuilder/commands/InitTest.cs index 9f8a968..9bca9df 100644 --- a/TestResumeBuilder/commands/InitTest.cs +++ b/TestResumeBuilder/commands/InitTest.cs @@ -19,8 +19,8 @@ public void Init_WithNoArgs_ShouldCreateDb() //when var result = TestApp.Run("init"); //then - Assert.True(File.Exists("TestResumeBuilder.db")); Assert.Equal(0, result.ExitCode); + Assert.True(File.Exists("resume.db")); TestDb = new ResumeContext(); Assert.True(TestDb.Database.CanConnect()); } diff --git a/TestResumeBuilder/commands/add/AddProfileTest.cs b/TestResumeBuilder/commands/add/AddProfileTest.cs new file mode 100644 index 0000000..45db1f4 --- /dev/null +++ b/TestResumeBuilder/commands/add/AddProfileTest.cs @@ -0,0 +1,242 @@ +using resume_builder.cli.commands.add; +using resume_builder.models; +using TestResumeBuilder.data; + +namespace TestResumeBuilder.commands.add; + +public class AddProfileTest: TestBase +{ + private readonly string[] _cmdArgs = { "add", "profile" }; + + [Fact] + public void AddProfile_WithNoArgs_ShouldSucceed() + { + //when + var result = TestApp.Run(_cmdArgs); + //then + Assert.Equal(0, result.ExitCode); + Assert.NotEmpty(TestDb.Profiles); + } + + [Fact] + public void AddProfile_NonInteractive_WithNoArgs_ShouldFail() + { + //when + var result = TestApp.Run("add", "-i", "false", "job"); + //then + Assert.Equal(0, result.ExitCode); + Assert.NotEmpty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.AllOptions), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithAllOptions_ShouldSucceed(Profile profile) + { + //given + var firstName = profile.FirstName; + var middleName = profile.MiddleName; + var lastName = profile.LastName; + var email = profile.EmailAddress; + var phone = profile.PhoneNumber; + var website = profile.Website; + var summary = profile.Summary; + var args = CreateCmdOptions(profile); + //when + var result = TestApp.Run(args); + var resultSettings = result.Settings as AddProfileSettings; + //then + Assert.Equal(0, result.ExitCode); + Assert.NotNull(resultSettings); + Assert.Multiple(() => { + Assert.Equal(firstName, resultSettings.FirstName); + Assert.Equal(middleName, resultSettings.MiddleName); + Assert.Equal(lastName, resultSettings.LastName); + Assert.Equal(email, resultSettings.EmailAddress); + Assert.Equal(phone, resultSettings.PhoneNumber); + Assert.Equal(website, resultSettings.Website); + Assert.Equal(summary, resultSettings.Summary); + }); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.AllOptions), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithAnEmptyFirstname_AndAllOptions_ShouldFail(Profile profile) + { + //given + var args = CreateCmdOptions(string.Empty, profile.LastName, profile.EmailAddress, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //when + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.WhiteSpaceStringAndProfile), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithWhitespaceFirstname_AndAllOptions_ShouldFail(string firstName, Profile profile) + { + //given + var args = CreateCmdOptions(firstName, profile.LastName, profile.EmailAddress, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.AllOptions), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithAnEmptyLastname_AndAllOptions_ShouldFail(Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, string.Empty, profile.EmailAddress, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //when + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.WhiteSpaceStringAndProfile), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithWhitespaceLastname_AndAllOptions_ShouldFail(string lastname, Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, lastname, profile.EmailAddress, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.AllOptions), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithAnEmptyEmail_AndAllOptions_ShouldFail(Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, profile.LastName, string.Empty, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //when + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.WhiteSpaceStringAndProfile), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithWhitespaceEmail_AndAllOptions_ShouldFail(string email, Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, profile.LastName, email, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.InvalidEmails), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithInvalidEmail_ShouldFail(string email, Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, profile.LastName, null, profile.PhoneNumber, + profile.MiddleName, profile.Website, profile.Summary); + //then + Assert.ThrowsAny(() => TestApp.Run([..args, "-e", email])); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.AllOptions), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithAnEmptyPhoneNumber_AndAllOptions_ShouldFail(Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, profile.LastName, profile.EmailAddress, string.Empty, + profile.MiddleName, profile.Website, profile.Summary); + //when + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + [Theory] + [MemberData(nameof(AddProfileTestData.WhiteSpaceStringAndProfile), MemberType = typeof(AddProfileTestData))] + public void AddProfile_WithWhitespacePhoneNumber_AndAllOptions_ShouldFail(string phone, Profile profile) + { + //given + var args = CreateCmdOptions(profile.FirstName, profile.LastName, profile.EmailAddress, phone, + profile.MiddleName, profile.Website, profile.Summary); + //then + Assert.ThrowsAny(() => TestApp.Run(args)); + Assert.Empty(TestDb.Profiles); + } + + + private string[] CreateCmdOptions(Profile profile) + { + var firstName = profile.FirstName; + var middleName = profile.MiddleName; + var lastName = profile.LastName; + var email = profile.EmailAddress; + var phone = profile.PhoneNumber; + var website = profile.Website; + var summary = profile.Summary; + + string[] args = [.._cmdArgs, "-f", firstName, "-l", lastName, "-e", email, "-p", phone]; + if(middleName != null) + args = args.Concat(new[] { "-m", middleName }).ToArray(); + if(website != null) + args = args.Concat(new[] { "-w", website }).ToArray(); + if(summary != null) + args = [.. args, "-s", summary]; + return args; + } + + private string[] CreateCmdOptions(string? firstName = null, string? lastName = null, string? email = null, + string? phone = null, + string? middleName = null, string? website = null, string? summary = null) + { + var args = _cmdArgs; + if(firstName != null) + args = [..args, "-f", firstName]; + if(lastName != null) + args = [..args, "-l", lastName]; + if(email != null) + args = [..args, "-e", email]; + if(phone != null) + args = [..args, "-p", phone]; + if(middleName != null) + args = [..args, "-m", middleName]; + if(website != null) + args = [..args, "-w", website]; + if(summary != null) + args = [.. args, "-s", summary]; + return args; + } +} + +internal class AddProfileTestData: ProfileTestData +{ + public static TheoryData InvalidEmails() + { + var data = new TheoryData(); + for(var i = 0; i < TestRepetitions; i++) + data.Add(Faker.Random.String().Replace("@", ""), GetFakeProfile()); + return data; + } + + public static TheoryData WhiteSpaceStringAndProfile() + { + var data = new TheoryData(); + foreach(var whitespace in RandomWhiteSpaceString()) + data.Add(whitespace, GetFakeProfile()); + return data; + } + + public static TheoryData AllOptions() + { + var data = new TheoryData(); + for(var i = TestRepetitions - 1; i >= 0; i--) + data.Add(ProfileTestData.GetFakeProfile()); + return data; + } +} \ No newline at end of file diff --git a/TestResumeBuilder/data/ProfileTestData.cs b/TestResumeBuilder/data/ProfileTestData.cs new file mode 100644 index 0000000..addb173 --- /dev/null +++ b/TestResumeBuilder/data/ProfileTestData.cs @@ -0,0 +1,22 @@ +using AutoBogus; +using AutoBogus.Conventions; +using Bogus; +using resume_builder.models; + +namespace TestResumeBuilder.data; + +internal class ProfileTestData: TestData +{ + static public Faker BogusProfile = new AutoFaker() + .Configure(config => config.WithConventions(conv => + conv.Email.Aliases("EmailAddress", "emailAddress"))) + .RuleFor(profile => profile.MiddleName, + Faker.Random.Word().OrNull(Faker)) + //.RuleFor(profile => profile.EmailAddress, (_, profile) => Faker.Internet.Email(profile.FirstName, profile.LastName)) + .RuleFor(profile => profile.Summary, + Faker.Lorem.Paragraph().OrNull(Faker)); + + static public IEnumerable InfiniteFakeProfiles => BogusProfile.GenerateForever(); + static public List GetFakeProfiles(int count = TestRepetitions) => BogusProfile.Generate(count); + static public Profile GetFakeProfile() => BogusProfile.Generate(); +} \ No newline at end of file diff --git a/TestResumeBuilder/data/TestData.cs b/TestResumeBuilder/data/TestData.cs index 571de14..7f45548 100644 --- a/TestResumeBuilder/data/TestData.cs +++ b/TestResumeBuilder/data/TestData.cs @@ -7,6 +7,25 @@ internal class TestData { private protected const int TestRepetitions = 10; + public const string space = " "; + public const string tab = "\t"; + public const string newline = "\n"; + public const string zeroWidthSpace = "\u200b"; + public const string zeroWidthJoiner = "\u200d"; + public const string zeroWidthNonJoiner = "\u200c"; + public const string zeroWidthNoBreakSpace = "\u200b"; + public const string zeroWidthHairSpace = "\u200a"; + public const string sixPerEmSpace = "\u2006"; + public const string thinSpace = "\u2009"; + public const string punctuationSpace = "\u2008"; + public const string fourPerEmSpace = "\u2005"; + public const string threePerEmSpace = "\u2004"; + public const string figureSpace = "\u2007"; + public const string enSpace = "\u2002"; + public const string emSpace = "\u2003"; + + public const string braillePatternBlank = "\u2800"; + private static Random Random { get; set; } = new(); protected static Faker Faker { get; set; } = new(); @@ -35,4 +54,27 @@ static private protected int MaxRandomYearsBeforeToday() public static string Waffle() => WaffleEngine.Text(Random.Next(TestRepetitions), Faker.Random.Bool()); public static string? WaffleOrNull() => Waffle().OrNull(Faker); + + //#region white spaces + public static IEnumerable RandomWhiteSpaceString(int count = TestRepetitions) + { + yield return Faker.Random.String2(Random.Next(count), space); + yield return Faker.Random.String2(Random.Next(count), tab); + yield return Faker.Random.String2(Random.Next(count), newline); + yield return Faker.Random.String2(Random.Next(count), zeroWidthSpace); + yield return Faker.Random.String2(Random.Next(count), zeroWidthJoiner); + yield return Faker.Random.String2(Random.Next(count), zeroWidthNonJoiner); + yield return Faker.Random.String2(Random.Next(count), zeroWidthNoBreakSpace); + yield return Faker.Random.String2(Random.Next(count), zeroWidthHairSpace); + yield return Faker.Random.String2(Random.Next(count), sixPerEmSpace); + yield return Faker.Random.String2(Random.Next(count), thinSpace); + yield return Faker.Random.String2(Random.Next(count), punctuationSpace); + yield return Faker.Random.String2(Random.Next(count), fourPerEmSpace); + yield return Faker.Random.String2(Random.Next(count), threePerEmSpace); + yield return Faker.Random.String2(Random.Next(count), figureSpace); + yield return Faker.Random.String2(Random.Next(count), enSpace); + yield return Faker.Random.String2(Random.Next(count), emSpace); + yield return Faker.Random.String2(Random.Next(count), braillePatternBlank); + } + //#endregion } \ No newline at end of file diff --git a/resume builder/cli/commands/add/AddProfileCommand.cs b/resume builder/cli/commands/add/AddProfileCommand.cs index bfc77b2..93cea42 100644 --- a/resume builder/cli/commands/add/AddProfileCommand.cs +++ b/resume builder/cli/commands/add/AddProfileCommand.cs @@ -1,6 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using Microsoft.Data.Sqlite; -using resume_builder.models; using resume_builder.models; using Spectre.Console; using Spectre.Console.Cli; @@ -8,7 +6,7 @@ namespace resume_builder.cli.commands.add; -public sealed class AddProfileCommand : Command +public sealed class AddProfileCommand: Command { public override int Execute([NotNull] CommandContext context, [NotNull] AddProfileSettings settings) { @@ -22,53 +20,63 @@ public override int Execute([NotNull] CommandContext context, [NotNull] AddProfi if(settings.PromptUser) { - var firstNamePrompt = new TextPrompt("First name: "); + var firstNamePrompt = RenderableFactory.CreateTextPrompt("First name: ", firstName); + var lastNamePrompt = RenderableFactory.CreateTextPrompt("Last name: ", lastName); + var middleNamePrompt = RenderableFactory.CreateTextPrompt("Middle name: ", middleName).AllowEmpty(); + var phoneNumberPrompt = RenderableFactory.CreateTextPrompt("Phone number: ", phoneNumber); + var emailAddressPrompt = RenderableFactory.CreateTextPrompt("Email address: ", emailAddress); + var websitePrompt = RenderableFactory.CreateTextPrompt("Website: ", website).AllowEmpty(); + var summaryPrompt = RenderableFactory.CreateTextPrompt("Summary: ", summary).AllowEmpty(); + + firstName = AnsiConsole.Prompt(firstNamePrompt); + lastName = AnsiConsole.Prompt(lastNamePrompt); + middleName = AnsiConsole.Prompt(middleNamePrompt); + phoneNumber = AnsiConsole.Prompt(phoneNumberPrompt); + emailAddress = AnsiConsole.Prompt(emailAddressPrompt); + website = AnsiConsole.Prompt(websitePrompt); + summary = AnsiConsole.Prompt(summaryPrompt); } - var profile = new Profile(settings.FirstName, settings.LastName, settings.PhoneNumber, settings.EmailAddress); + var profile = new Profile(firstName, lastName, phoneNumber, emailAddress) + { + MiddleName = middleName, + Website = website, + Summary = summary + }; profile.Summary = settings.Summary; profile.Website = settings.Website; ResumeContext database = new(); - try - { - database.Profiles.Add(profile); - database.SaveChanges(); - } - catch(Exception e) - { - return Globals.PrintError(settings, e); - } + + database.Profiles.Add(profile); + database.SaveChanges(); AnsiConsole.MarkupLine($"✅ profile: [BOLD]{profile.FullName}[/] added"); return ExitCode.Success.ToInt(); } } -public class AddProfileSettings : AddCommandSettings +public class AddProfileSettings: AddCommandSettings { public bool PromptUser => (FirstName.IsBlank() && LastName.IsBlank() && MiddleName.IsBlank() && PhoneNumber.IsBlank() && EmailAddress.IsBlank() && Website.IsBlank() && Summary.IsBlank()) || - (!FirstName.IsBlank() && !LastName.IsBlank() && !MiddleName.IsBlank() && !PhoneNumber.IsBlank() && - !EmailAddress.IsBlank() && !Website.IsBlank() && !Summary.IsBlank()) - || Interactive; [CommandOption("-f|--first ")] - public string FirstName { get; set; } + public string? FirstName { get; set; } [CommandOption("-m|--middle ")] public string? MiddleName { get; init; } [CommandOption("-l|--last ")] - public string LastName { get; set; } + public string? LastName { get; set; } [CommandOption("-p|--phone ")] public string PhoneNumber { get; set; } [CommandOption("-e|--email ")] - public string EmailAddress { get; set; } + public string? EmailAddress { get; set; } [CommandOption("-w|--website ")] public string? Website { get; set; } @@ -78,14 +86,21 @@ public class AddProfileSettings : AddCommandSettings public override ValidationResult Validate() { + if(PromptUser) + return ValidationResult.Success(); if(string.IsNullOrWhiteSpace(FirstName)) return ValidationResult.Error(FirstName == null ? "First name is required" : "First name cannot be empty"); + if(string.IsNullOrWhiteSpace(LastName)) + return ValidationResult.Error(LastName == null ? "Last name is required" : "Last name cannot be empty"); + if(string.IsNullOrWhiteSpace(EmailAddress)) return ValidationResult.Error("Email address is required"); if(!EmailAddress.Contains('@')) return ValidationResult.Error("Email address invalid: must contain '@'"); + if(string.IsNullOrWhiteSpace(PhoneNumber)) return ValidationResult.Error("Phone number is required"); + return ValidationResult.Success(); } } \ No newline at end of file