diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0acf42d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,145 @@ +name: CI Pipeline + +on: + push: + branches: [main, develop, 'feature/**'] + pull_request: + branches: [main, develop] + +jobs: + build: + name: Build & Restore + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + unit-tests: + name: Unit Tests & Coverage + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore + run: dotnet restore + + - name: Run Unit Tests with Coverage + run: | + dotnet test BPCalculator.Tests/BPCalculator.Tests.csproj \ + --configuration Release \ + --collect:"XPlat Code Coverage" \ + --results-directory ./coverage \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Exclude="[*]BPCalculator.Pages.Pages_*,[*]BPCalculator.Pages.ErrorModel,[*]BPCalculator.Pages.PrivacyModel,[*]BPCalculator.Program,[*]BPCalculator.Startup" + + - name: Install ReportGenerator + run: dotnet tool install -g dotnet-reportgenerator-globaltool + + - name: Generate Coverage Report + run: | + reportgenerator \ + -reports:./coverage/**/coverage.cobertura.xml \ + -targetdir:./coverage-html \ + -reporttypes:Html + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: ./coverage-html + + bdd-tests: + name: BDD Tests + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore + run: dotnet restore + + - name: Run BDD Tests + run: | + dotnet test BPCalculator.BDDTests/BPCalculator.BDDTests.csproj \ + --configuration Release + + security: + name: Security Scans + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Gitleaks Secret Scan + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Restore + run: dotnet restore + + - name: Snyk Dependency Scan + uses: snyk/actions/dotnet@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high + + sonarcloud: + name: SonarCloud Analysis + runs-on: ubuntu-latest + needs: unit-tests + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Install SonarScanner + run: dotnet tool install --global dotnet-sonarscanner + + - name: SonarCloud Scan + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet sonarscanner begin \ + /k:"${{ secrets.SONAR_PROJECT_KEY }}" \ + /o:"${{ secrets.SONAR_ORG_KEY }}" \ + /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + dotnet build --no-incremental + dotnet sonarscanner end \ + /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/BPCalculator.BDDTests/BPCalculator.BDDTests.csproj b/BPCalculator.BDDTests/BPCalculator.BDDTests.csproj new file mode 100644 index 0000000..64f35f7 --- /dev/null +++ b/BPCalculator.BDDTests/BPCalculator.BDDTests.csproj @@ -0,0 +1,26 @@ + + + net9.0 + enable + enable + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/BPCalculator.BDDTests/Class1.cs b/BPCalculator.BDDTests/Class1.cs new file mode 100644 index 0000000..43e4e62 --- /dev/null +++ b/BPCalculator.BDDTests/Class1.cs @@ -0,0 +1,6 @@ +namespace BPCalculator.BDDTests; + +public class Class1 +{ + +} diff --git a/BPCalculator.BDDTests/Features/BloodPressure.feature b/BPCalculator.BDDTests/Features/BloodPressure.feature new file mode 100644 index 0000000..1a090fa Binary files /dev/null and b/BPCalculator.BDDTests/Features/BloodPressure.feature differ diff --git a/BPCalculator.BDDTests/Features/BloodPressure.feature.cs b/BPCalculator.BDDTests/Features/BloodPressure.feature.cs new file mode 100644 index 0000000..10ea74c --- /dev/null +++ b/BPCalculator.BDDTests/Features/BloodPressure.feature.cs @@ -0,0 +1,340 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace BPCalculator.BDDTests.Features +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public partial class BloodPressureCategoryCalculationFeature : object, Xunit.IClassFixture, System.IDisposable + { + + private static TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = ((string[])(null)); + + private Xunit.Abstractions.ITestOutputHelper _testOutputHelper; + +#line 1 "BloodPressure.feature" +#line hidden + + public BloodPressureCategoryCalculationFeature(BloodPressureCategoryCalculationFeature.FixtureData fixtureData, BPCalculator_BDDTests_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper) + { + this._testOutputHelper = testOutputHelper; + this.TestInitialize(); + } + + public static void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Blood Pressure Category Calculation", " As a patient\r\n I want to know my blood pressure category\r\n So I can understan" + + "d my health status and take action", ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + public static void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + public void TestInitialize() + { + } + + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + void System.IDisposable.Dispose() + { + this.TestTearDown(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Low blood pressure reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Low blood pressure reading")] + public void LowBloodPressureReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Low blood pressure reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 6 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 7 + testRunner.Given("the patient has a systolic pressure of 85", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 8 + testRunner.And("the patient has a diastolic pressure of 55", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 9 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 10 + testRunner.Then("the category should be \"Low Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Ideal blood pressure reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Ideal blood pressure reading")] + public void IdealBloodPressureReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Ideal blood pressure reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 12 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 13 + testRunner.Given("the patient has a systolic pressure of 110", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 14 + testRunner.And("the patient has a diastolic pressure of 70", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 15 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 16 + testRunner.Then("the category should be \"Ideal Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Pre-high blood pressure reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Pre-high blood pressure reading")] + public void Pre_HighBloodPressureReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Pre-high blood pressure reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 18 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 19 + testRunner.Given("the patient has a systolic pressure of 130", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 20 + testRunner.And("the patient has a diastolic pressure of 85", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 21 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 22 + testRunner.Then("the category should be \"Pre-High Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="High blood pressure reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "High blood pressure reading")] + public void HighBloodPressureReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("High blood pressure reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 24 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 25 + testRunner.Given("the patient has a systolic pressure of 150", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 26 + testRunner.And("the patient has a diastolic pressure of 95", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 27 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 28 + testRunner.Then("the category should be \"High Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Boundary - lowest ideal reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Boundary - lowest ideal reading")] + public void Boundary_LowestIdealReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Boundary - lowest ideal reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 30 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 31 + testRunner.Given("the patient has a systolic pressure of 90", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 32 + testRunner.And("the patient has a diastolic pressure of 60", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 33 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 34 + testRunner.Then("the category should be \"Ideal Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Boundary - lowest high reading")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Boundary - lowest high reading")] + public void Boundary_LowestHighReading() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Boundary - lowest high reading", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 36 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 37 + testRunner.Given("the patient has a systolic pressure of 140", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 38 + testRunner.And("the patient has a diastolic pressure of 90", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 39 + testRunner.When("the blood pressure category is calculated", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 40 + testRunner.Then("the category should be \"High Blood Pressure\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [Xunit.SkippableFactAttribute(DisplayName="Invalid reading - systolic not greater than diastolic")] + [Xunit.TraitAttribute("FeatureTitle", "Blood Pressure Category Calculation")] + [Xunit.TraitAttribute("Description", "Invalid reading - systolic not greater than diastolic")] + public void InvalidReading_SystolicNotGreaterThanDiastolic() + { + string[] tagsOfScenario = ((string[])(null)); + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Invalid reading - systolic not greater than diastolic", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 42 + this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 43 + testRunner.Given("the patient has a systolic pressure of 80", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 44 + testRunner.And("the patient has a diastolic pressure of 90", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 45 + testRunner.When("the blood pressure validity is checked", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 46 + testRunner.Then("the reading should be invalid", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class FixtureData : System.IDisposable + { + + public FixtureData() + { + BloodPressureCategoryCalculationFeature.FeatureSetup(); + } + + void System.IDisposable.Dispose() + { + BloodPressureCategoryCalculationFeature.FeatureTearDown(); + } + } + } +} +#pragma warning restore +#endregion diff --git a/BPCalculator.BDDTests/StepDefinitions/BpSteps.cs b/BPCalculator.BDDTests/StepDefinitions/BpSteps.cs new file mode 100644 index 0000000..69cfd67 --- /dev/null +++ b/BPCalculator.BDDTests/StepDefinitions/BpSteps.cs @@ -0,0 +1,53 @@ +using BPCalculator; +using TechTalk.SpecFlow; +using Xunit; + +namespace BPCalculator.BDDTests.StepDefinitions +{ + [Binding] + public class BpSteps + { + private int _systolic; + private int _diastolic; + private string _category = string.Empty; + private bool _isValid; + + [Given(@"the patient has a systolic pressure of (.*)")] + public void GivenSystolic(int value) + { + _systolic = value; + } + + [Given(@"the patient has a diastolic pressure of (.*)")] + public void GivenDiastolic(int value) + { + _diastolic = value; + } + + [When(@"the blood pressure category is calculated")] + public void WhenCategoryCalculated() + { + var bp = new BloodPressure { Systolic = _systolic, Diastolic = _diastolic }; + _category = bp.Category.GetDisplayName(); + } + + [When(@"the blood pressure validity is checked")] + public void WhenValidityChecked() + { + var bp = new BloodPressure { Systolic = _systolic, Diastolic = _diastolic }; + _isValid = bp.IsValid(); + } + + [Then(@"the category should be ""(.*)""")] + public void ThenCategory(string expected) + { + Assert.Equal(expected, _category); + } + + [Then(@"the reading should be invalid")] + public void ThenInvalid() + { + Assert.False(_isValid); + } + } +} \ No newline at end of file diff --git a/BPCalculator.BDDTests/StepDefinitions/EnumExtensions.cs b/BPCalculator.BDDTests/StepDefinitions/EnumExtensions.cs new file mode 100644 index 0000000..1c19f14 --- /dev/null +++ b/BPCalculator.BDDTests/StepDefinitions/EnumExtensions.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace BPCalculator.BDDTests.StepDefinitions +{ + public static class EnumExtensions + { + public static string GetDisplayName(this Enum enumValue) + { + return enumValue.GetType() + .GetMember(enumValue.ToString()) + .First() + .GetCustomAttribute() + ?.Name ?? enumValue.ToString(); + } + } +} diff --git a/BPCalculator.Tests/BPCalculator.Tests.csproj b/BPCalculator.Tests/BPCalculator.Tests.csproj new file mode 100644 index 0000000..fe792b0 --- /dev/null +++ b/BPCalculator.Tests/BPCalculator.Tests.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + diff --git a/BPCalculator.Tests/UnitTest1.cs b/BPCalculator.Tests/UnitTest1.cs new file mode 100644 index 0000000..4440741 --- /dev/null +++ b/BPCalculator.Tests/UnitTest1.cs @@ -0,0 +1,226 @@ +using Moq; +using BPCalculator; +using BPCalculator.Pages; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; + +namespace BPCalculator.Tests +{ + public class BloodPressureTests + { + // ── CATEGORY TESTS ────────────────────────────────────────── + + [Theory] + [InlineData(85, 55, BPCategory.Low)] // normal low + [InlineData(89, 59, BPCategory.Low)] // boundary — just below ideal + [InlineData(80, 60, BPCategory.Low)] // systolic low even if diastolic ok + public void Category_ReturnsLow_WhenBPIsLow(int systolic, int diastolic, BPCategory expected) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.Category.Should().Be(expected); + } + + [Theory] + [InlineData(90, 60, BPCategory.Ideal)] // boundary — lower limit inclusive + [InlineData(110, 70, BPCategory.Ideal)] // normal ideal + [InlineData(119, 79, BPCategory.Ideal)] // boundary — just below pre-high + public void Category_ReturnsIdeal_WhenBPIsIdeal(int systolic, int diastolic, BPCategory expected) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.Category.Should().Be(expected); + } + + [Theory] + [InlineData(120, 80, BPCategory.PreHigh)] // boundary — lower limit inclusive + [InlineData(130, 85, BPCategory.PreHigh)] // normal pre-high + [InlineData(139, 89, BPCategory.PreHigh)] // boundary — just below high + public void Category_ReturnsPreHigh_WhenBPIsPreHigh(int systolic, int diastolic, BPCategory expected) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.Category.Should().Be(expected); + } + + [Theory] + [InlineData(140, 90, BPCategory.High)] // boundary — lower limit inclusive + [InlineData(160, 95, BPCategory.High)] // normal high + [InlineData(180, 100, BPCategory.High)] // maximum valid values + public void Category_ReturnsHigh_WhenBPIsHigh(int systolic, int diastolic, BPCategory expected) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.Category.Should().Be(expected); + } + + // ── ISVALID TESTS ──────────────────────────────────────────── + + [Theory] + [InlineData(120, 80)] // normal valid + [InlineData(90, 60)] // boundary valid + [InlineData(150, 95)] // high but valid + public void IsValid_ReturnsTrue_WhenSystolicGreaterThanDiastolic(int systolic, int diastolic) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.IsValid().Should().BeTrue(); + } + + [Theory] + [InlineData(80, 90)] // diastolic higher — invalid + [InlineData(70, 70)] // equal — invalid + public void IsValid_ReturnsFalse_WhenSystolicNotGreaterThanDiastolic(int systolic, int diastolic) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.IsValid().Should().BeFalse(); + } + + // ── RECOMMENDATION TESTS ───────────────────────────────────── + + [Fact] + public void Recommendation_IsNotEmpty_ForAllCategories() + { + var readings = new[] + { + new BloodPressure { Systolic = 85, Diastolic = 55 }, + new BloodPressure { Systolic = 110, Diastolic = 70 }, + new BloodPressure { Systolic = 130, Diastolic = 85 }, + new BloodPressure { Systolic = 150, Diastolic = 95 }, + }; + + foreach (var bp in readings) + { + bp.Recommendation.Should().NotBeNullOrEmpty( + because: $"category {bp.Category} should always have a recommendation"); + } + } + + [Fact] + public void Recommendation_IsDifferent_ForEachCategory() + { + var low = new BloodPressure { Systolic = 85, Diastolic = 55 }; + var ideal = new BloodPressure { Systolic = 110, Diastolic = 70 }; + var preHigh = new BloodPressure { Systolic = 130, Diastolic = 85 }; + var high = new BloodPressure { Systolic = 150, Diastolic = 95 }; + + var recommendations = new[] + { + low.Recommendation, + ideal.Recommendation, + preHigh.Recommendation, + high.Recommendation + }; + + recommendations.Should().OnlyHaveUniqueItems( + because: "each category should return a distinct recommendation"); + } + + [Theory] + [InlineData(85, 55, "low")] + [InlineData(110, 70, "ideal")] + [InlineData(130, 85, "elevated")] + [InlineData(150, 95, "high")] + public void Recommendation_ContainsExpectedKeyword(int systolic, int diastolic, string keyword) + { + var bp = new BloodPressure { Systolic = systolic, Diastolic = diastolic }; + bp.Recommendation.ToLower().Should().Contain(keyword); + } + + // ── PAGE MODEL TESTS ────────────────────────────────────────── + + private BloodPressureModel CreateModel() +{ + var model = new BloodPressureModel(); + var httpContext = new DefaultHttpContext(); + var modelState = new ModelStateDictionary(); + var actionContext = new ActionContext( + httpContext, + new RouteData(), + new PageActionDescriptor(), + modelState); + var modelMetadata = new EmptyModelMetadataProvider(); + var viewData = new ViewDataDictionary(modelMetadata, modelState); + var tempData = new TempDataDictionary(httpContext, + Mock.Of()); + + model.PageContext = new PageContext(actionContext) + { + ViewData = viewData + }; + model.TempData = tempData; + + return model; +} + + + [Fact] + public void OnGet_SetsDefaultBPValues() + { + var model = CreateModel(); + model.OnGet(); + + model.BP.Should().NotBeNull(); + model.BP.Systolic.Should().Be(120); + model.BP.Diastolic.Should().Be(80); + } + + [Fact] + public void OnPost_ReturnsPage_WhenBPIsValid() + { + var model = CreateModel(); + model.BP = new BloodPressure { Systolic = 120, Diastolic = 80 }; + + var result = model.OnPost(); + + result.Should().BeOfType(); + } + + [Fact] + public void OnPost_AddsModelError_WhenSystolicNotGreaterThanDiastolic() + { + var model = CreateModel(); + model.BP = new BloodPressure { Systolic = 80, Diastolic = 90 }; + + model.OnPost(); + + model.ModelState.IsValid.Should().BeFalse(); + model.ModelState[string.Empty]!.Errors.Should().NotBeEmpty(); + } + + [Fact] + public void OnPost_AddsModelError_WhenSystolicEqualsDiastolic() + { + var model = CreateModel(); + model.BP = new BloodPressure { Systolic = 80, Diastolic = 80 }; + + model.OnPost(); + + model.ModelState.IsValid.Should().BeFalse(); + } + + [Fact] + public void OnPost_ReturnsPage_WhenHighBP() + { + var model = CreateModel(); + model.BP = new BloodPressure { Systolic = 160, Diastolic = 95 }; + + var result = model.OnPost(); + + result.Should().BeOfType(); + model.BP.Category.Should().Be(BPCategory.High); + } + + [Fact] + public void OnPost_ReturnsPage_WhenLowBP() + { + var model = CreateModel(); + model.BP = new BloodPressure { Systolic = 85, Diastolic = 55 }; + + var result = model.OnPost(); + + result.Should().BeOfType(); + model.BP.Category.Should().Be(BPCategory.Low); + } + } +} diff --git a/BPCalculator.Tests/coverage/3e61289d-497b-43c9-a42e-42582ade91de/coverage.cobertura.xml b/BPCalculator.Tests/coverage/3e61289d-497b-43c9-a42e-42582ade91de/coverage.cobertura.xml new file mode 100644 index 0000000..8feae7c --- /dev/null +++ b/BPCalculator.Tests/coverage/3e61289d-497b-43c9-a42e-42582ade91de/coverage.cobertura.xml @@ -0,0 +1,184 @@ + + + + C:\Users\NILAVAN\CI-CD-Pipeline-for-Blood-Pressure-Category-Calculator\BPCalculator\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BPCalculator/BloodPressure.cs b/BPCalculator/BloodPressure.cs index 3e8fee9..e64f719 100644 --- a/BPCalculator/BloodPressure.cs +++ b/BPCalculator/BloodPressure.cs @@ -1,40 +1,66 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; namespace BPCalculator { - // BP categories public enum BPCategory { - [Display(Name="Low Blood Pressure")] Low, - [Display(Name="Ideal Blood Pressure")] Ideal, - [Display(Name="Pre-High Blood Pressure")] PreHigh, - [Display(Name ="High Blood Pressure")] High + [Display(Name = "Low Blood Pressure")] Low, + [Display(Name = "Ideal Blood Pressure")] Ideal, + [Display(Name = "Pre-High Blood Pressure")] PreHigh, + [Display(Name = "High Blood Pressure")] High }; public class BloodPressure { - public const int SystolicMin = 70; - public const int SystolicMax = 190; + public const int SystolicMin = 70; + public const int SystolicMax = 190; public const int DiastolicMin = 40; public const int DiastolicMax = 100; - [Range(SystolicMin, SystolicMax, ErrorMessage = "Invalid Systolic Value")] - public int Systolic { get; set; } // mmHG + [Range(SystolicMin, SystolicMax, + ErrorMessage = "Systolic must be between 70 and 190")] + public int Systolic { get; set; } - [Range(DiastolicMin, DiastolicMax, ErrorMessage = "Invalid Diastolic Value")] - public int Diastolic { get; set; } // mmHG + [Range(DiastolicMin, DiastolicMax, + ErrorMessage = "Diastolic must be between 40 and 100")] + public int Diastolic { get; set; } + + // returns true only when systolic is strictly greater than diastolic + public bool IsValid() => Systolic > Diastolic; // calculate BP category public BPCategory Category { get { - // implement as part of project - //throw new NotImplementedException("not implemented yet"); - return new BPCategory(); // replace this + if (Systolic < 90 || Diastolic < 60) + return BPCategory.Low; + + if (Systolic < 120 && Diastolic < 80) + return BPCategory.Ideal; + + if (Systolic < 140 && Diastolic < 90) + return BPCategory.PreHigh; + + return BPCategory.High; + } + } + + // NEW FEATURE — health recommendation (max 30 lines) + public string Recommendation + { + get + { + return Category switch + { + BPCategory.Low => "Your BP is low. Consider increasing fluid and salt intake. Consult your GP if you feel dizzy or faint.", + BPCategory.Ideal => "Your BP is ideal. Keep up your healthy lifestyle — regular exercise and a balanced diet.", + BPCategory.PreHigh => "Your BP is slightly elevated. Reduce salt intake, exercise regularly, and monitor it closely.", + BPCategory.High => "Your BP is high. Please consult a doctor as soon as possible.", + _ => string.Empty + }; } } } -} +} \ No newline at end of file diff --git a/BPCalculator/Pages/Index.cshtml b/BPCalculator/Pages/Index.cshtml index 26fd873..375729f 100644 --- a/BPCalculator/Pages/Index.cshtml +++ b/BPCalculator/Pages/Index.cshtml @@ -1,40 +1,79 @@ @page @model BPCalculator.Pages.BloodPressureModel - @{ ViewData["Title"] = "BP Category Calculator"; } -

BP Category Calculator


+
-
+
+
-
+ +
- +
-
+ +
-
- + +
+
- @if (ViewData.ModelState.IsValid) + + @if (ViewData.ModelState.IsValid && Model.BP != null) { -
- @Html.DisplayFor(model => model.BP.Category, new { htmlAttributes = new { @class = "form-control" } }) +
+ Category: + @Html.DisplayFor(model => model.BP.Category) +
+ +
+ Recommendation: + @Model.BP.Recommendation
} +
@section Scripts { - @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } } +``` + +--- + +## Now do this in order: + +**1. Replace all three files** — copy each block above into the correct file in VS Code. Select all existing content (`Ctrl+A`) and paste. + +**2. Run the app:** +``` +dotnet run +``` + +**3. Test these inputs manually:** + +| Systolic | Diastolic | Expected | +|---|---|---| +| 150 | 95 | High Blood Pressure | +| 110 | 70 | Ideal Blood Pressure | +| 85 | 55 | Low Blood Pressure | +| 130 | 85 | Pre-High Blood Pressure | +| 80 | 90 | Error — systolic must be greater | + +**4. Once all 5 tests show the right result, commit everything:** +``` +git add . +git commit -m "Complete BP logic, validation and recommendation feature" +git push origin feature/bp-logic \ No newline at end of file diff --git a/BPCalculator/Pages/Index.cshtml.cs b/BPCalculator/Pages/Index.cshtml.cs index f96dc16..7c889d3 100644 --- a/BPCalculator/Pages/Index.cshtml.cs +++ b/BPCalculator/Pages/Index.cshtml.cs @@ -1,29 +1,36 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -// page model - namespace BPCalculator.Pages { public class BloodPressureModel : PageModel { - [BindProperty] // bound on POST + [BindProperty] public BloodPressure BP { get; set; } - // setup initial data + // setup initial data on page load public void OnGet() { - BP = new BloodPressure() { Systolic = 100, Diastolic = 60 }; + BP = new BloodPressure() { Systolic = 120, Diastolic = 80 }; } - // POST, validate + // handle form submission public IActionResult OnPost() { - // extra validation - if (!(BP.Systolic > BP.Diastolic)) + // check range validation from [Range] attributes first + if (!ModelState.IsValid) { - ModelState.AddModelError("", "Systolic must be greater than Diastolic"); + return Page(); } + + // extra validation — systolic must be greater than diastolic + if (!BP.IsValid()) + { + ModelState.AddModelError(string.Empty, + "Systolic pressure must be greater than Diastolic pressure."); + return Page(); + } + return Page(); } }