diff --git a/.github/workflows/core-build.yml b/.github/workflows/core-build.yml
index 83d1321b..52e46329 100644
--- a/.github/workflows/core-build.yml
+++ b/.github/workflows/core-build.yml
@@ -1,7 +1,7 @@
name: Build and test solution
env:
- DOTNET_VERSION: 6
+ DOTNET_VERSION: 8
on:
workflow_call:
diff --git a/coffeecard/CoffeeCard.Common/CoffeeCard.Common.csproj b/coffeecard/CoffeeCard.Common/CoffeeCard.Common.csproj
index b337e671..a5c9273e 100644
--- a/coffeecard/CoffeeCard.Common/CoffeeCard.Common.csproj
+++ b/coffeecard/CoffeeCard.Common/CoffeeCard.Common.csproj
@@ -1,11 +1,10 @@
- netstandard2.1
+ net8.0
33C1BE09-BD3A-4584-B195-C57B47B9063C
-
-
+
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Generators/Builder/BuilderGenerator.cs b/coffeecard/CoffeeCard.Generators/Builder/BuilderGenerator.cs
new file mode 100644
index 00000000..cf916185
--- /dev/null
+++ b/coffeecard/CoffeeCard.Generators/Builder/BuilderGenerator.cs
@@ -0,0 +1,134 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace CoffeeCard.Generators.Builder;
+
+[Generator]
+public class BuilderGenerator : IIncrementalGenerator
+{
+ private const string BuilderForAttribute = "CoffeeCard.Tests.Common.BuilderForAttribute";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var namedTypeSymbols = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: BuilderForAttribute,
+ predicate: IsSyntaxTargetForGeneration,
+ transform: GetSemanticTargetForGeneration)
+ .Where(t => t is not null).Collect();
+
+ context.RegisterSourceOutput(namedTypeSymbols, (productionContext, array) =>
+ {
+ foreach (var typeSymbol in array)
+ {
+ //Retrieve the entity it is a builder for
+ var entity = (INamedTypeSymbol)typeSymbol.GetAttributes()
+ .Single(attr => attr.AttributeClass.Name == "BuilderForAttribute").ConstructorArguments[0].Value;
+ var code = GenerateBuilderCode(typeSymbol, entity);
+ var sourceText = SourceText.From(code, Encoding.UTF8);
+ productionContext.AddSource($"{typeSymbol.Name}.g.cs", sourceText);
+ }
+ });
+ }
+
+ private static bool IsSyntaxTargetForGeneration(
+ SyntaxNode syntaxNode,
+ CancellationToken cancellationToken)
+ {
+ return syntaxNode is ClassDeclarationSyntax classDeclaration;
+ }
+
+ private static INamedTypeSymbol GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context,
+ CancellationToken cancellationToken)
+ {
+ return (INamedTypeSymbol)context.TargetSymbol;
+ }
+
+ private string GenerateBuilderCode(INamedTypeSymbol typeSymbol, ITypeSymbol entity)
+ {
+ var codeBuilder = new StringBuilder();
+
+ codeBuilder.AppendLine("// ");
+ codeBuilder.AppendLine("using System;");
+ codeBuilder.AppendLine("using AutoBogus.Conventions;");
+ codeBuilder.AppendLine($"using {entity.ContainingNamespace};");
+ codeBuilder.AppendLine();
+ codeBuilder.AppendLine($"namespace {typeSymbol.ContainingNamespace};");
+ codeBuilder.AppendLine();
+
+ codeBuilder.AppendLine($"public partial class {typeSymbol.Name} : BaseBuilder<{entity.Name}>, IBuilder<{typeSymbol.Name}>");
+ codeBuilder.AppendLine("{");
+
+ // Retrieve all properties of the given entity
+ var properties = entity.GetMembers().OfType().Where(p => p.Kind == SymbolKind.Property);
+
+ var entityNameChar = entity.Name.ToLowerInvariant()[0];
+ // Generate builder methods for all properties
+ var configBuilder = new StringBuilder();
+ foreach (var property in properties)
+ {
+ if (property.Name.Contains("Id"))
+ {
+ configBuilder.AppendLine($" .WithSkip<{entity.Name}>(\"{property.Name}\")");
+ }
+ AddWithPropertyValueToCodeBuilder(codeBuilder: codeBuilder,
+ typeSymbol: typeSymbol,
+ property: property,
+ entityNameChar: entityNameChar);
+
+ AddWithPropertySetterToCodeBuilder(
+ codeBuilder: codeBuilder,
+ typeSymbol: typeSymbol,
+ property: property,
+ entityNameChar: entityNameChar);
+ }
+ AddPrivateConstructorToCodeBuilder(codeBuilder, typeSymbol, configBuilder);
+
+ // End class
+ codeBuilder.AppendLine("}");
+
+ return codeBuilder.ToString();
+ }
+
+ ///
+ /// Generates a private constructor for the builder, to ensure the simple, or typical methods are used for instantiation
+ ///
+ ///
+ ///
+ ///
+ private void AddPrivateConstructorToCodeBuilder(StringBuilder codeBuilder, ITypeSymbol typeSymbol, StringBuilder configBuilder)
+ {
+ codeBuilder.AppendLine(
+ $" private {typeSymbol.Name} ()");
+ codeBuilder.AppendLine(" {");
+ codeBuilder.AppendLine(" Faker.Configure(builder => builder");
+ codeBuilder.Append($"{configBuilder}");
+ codeBuilder.AppendLine(" .WithConventions());");
+ codeBuilder.AppendLine(" }");
+ }
+
+ private void AddWithPropertyValueToCodeBuilder(StringBuilder codeBuilder, ITypeSymbol typeSymbol, IPropertySymbol property, char entityNameChar)
+ {
+ codeBuilder.AppendLine(
+ $" public {typeSymbol.Name} With{property.Name}({property.Type} {property.Name}Value)");
+ codeBuilder.AppendLine(" {");
+
+ codeBuilder.AppendLine(
+ $" Faker.RuleFor({entityNameChar} => {entityNameChar}.{property.Name}, {property.Name}Value);");
+ codeBuilder.AppendLine(" return this;");
+ codeBuilder.AppendLine(" }");
+ }
+ private void AddWithPropertySetterToCodeBuilder(StringBuilder codeBuilder, ITypeSymbol typeSymbol, IPropertySymbol property, char entityNameChar)
+ {
+ codeBuilder.AppendLine(
+ $" public {typeSymbol.Name} With{property.Name}(Func {property.Name}Setter)");
+ codeBuilder.AppendLine(" {");
+
+ codeBuilder.AppendLine(
+ $" Faker.RuleFor({entityNameChar} => {entityNameChar}.{property.Name}, {property.Name}Setter);");
+ codeBuilder.AppendLine(" return this;");
+ codeBuilder.AppendLine(" }");
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Generators/CoffeeCard.Generators.csproj b/coffeecard/CoffeeCard.Generators/CoffeeCard.Generators.csproj
new file mode 100644
index 00000000..70d54689
--- /dev/null
+++ b/coffeecard/CoffeeCard.Generators/CoffeeCard.Generators.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ enable
+ enable
+ 11.0
+
+
+ $(BaseIntermediateOutputPath)GeneratedCode\
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/coffeecard/CoffeeCard.Library/CoffeeCard.Library.csproj b/coffeecard/CoffeeCard.Library/CoffeeCard.Library.csproj
index e9b1c175..859831b4 100644
--- a/coffeecard/CoffeeCard.Library/CoffeeCard.Library.csproj
+++ b/coffeecard/CoffeeCard.Library/CoffeeCard.Library.csproj
@@ -1,46 +1,39 @@
-
-
- net6.0
- 8
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ net8.0
+ default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/coffeecard/CoffeeCard.MobilePay.GenerateApi/CoffeeCard.MobilePay.GenerateApi.csproj b/coffeecard/CoffeeCard.MobilePay.GenerateApi/CoffeeCard.MobilePay.GenerateApi.csproj
index a929ca87..a7558958 100644
--- a/coffeecard/CoffeeCard.MobilePay.GenerateApi/CoffeeCard.MobilePay.GenerateApi.csproj
+++ b/coffeecard/CoffeeCard.MobilePay.GenerateApi/CoffeeCard.MobilePay.GenerateApi.csproj
@@ -1,15 +1,12 @@
-
-
- Exe
- net6.0
- enable
- 8
-
-
-
-
-
-
-
+
+ Exe
+ net7.0
+ enable
+ default
+
+
+
+
+
diff --git a/coffeecard/CoffeeCard.MobilePay/CoffeeCard.MobilePay.csproj b/coffeecard/CoffeeCard.MobilePay/CoffeeCard.MobilePay.csproj
index 0a9ab86a..6af6845e 100644
--- a/coffeecard/CoffeeCard.MobilePay/CoffeeCard.MobilePay.csproj
+++ b/coffeecard/CoffeeCard.MobilePay/CoffeeCard.MobilePay.csproj
@@ -1,18 +1,17 @@
- net6.0
+ net8.0
AFF9D584-58F4-4A8D-A6BF-515890A9A3EF
enable
-
-
-
-
-
+
+
+
+
diff --git a/coffeecard/CoffeeCard.Models/CoffeeCard.Models.csproj b/coffeecard/CoffeeCard.Models/CoffeeCard.Models.csproj
index 33b2ba20..1e4e2bb0 100644
--- a/coffeecard/CoffeeCard.Models/CoffeeCard.Models.csproj
+++ b/coffeecard/CoffeeCard.Models/CoffeeCard.Models.csproj
@@ -1,22 +1,18 @@
-
-
- net6.0
- true
- enable
- default
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ net8.0
+ true
+ enable
+ default
+
+
+
+
+
+
+
+
+
+
+
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/FreeProductPaymentDetails.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/FreeProductPaymentDetails.cs
index 16ce980d..159a15d0 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/FreeProductPaymentDetails.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/FreeProductPaymentDetails.cs
@@ -1,6 +1,4 @@
using System.Runtime.Serialization;
-using System.Text.Json.Serialization;
-using NJsonSchema.Converters;
namespace CoffeeCard.Models.DataTransferObjects.v2.Purchase
{
@@ -14,7 +12,6 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Purchase
/// }
///
[KnownType(typeof(FreePurchasePaymentDetails))]
- [JsonConverter(typeof(JsonInheritanceConverter))]
public class FreePurchasePaymentDetails : PaymentDetails
{
///
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/MobilePayPaymentDetails.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/MobilePayPaymentDetails.cs
index 61b8215d..3fd4795d 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/MobilePayPaymentDetails.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/MobilePayPaymentDetails.cs
@@ -1,5 +1,4 @@
using Newtonsoft.Json;
-using NJsonSchema.Converters;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
@@ -17,7 +16,6 @@ namespace CoffeeCard.Models.DataTransferObjects.v2.Purchase
/// }
///
[KnownType(typeof(MobilePayPaymentDetails))]
- [JsonConverter(typeof(JsonInheritanceConverter))]
public class MobilePayPaymentDetails : PaymentDetails
{
///
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/PaymentDetails.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/PaymentDetails.cs
index 378d5a63..1b72271e 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/PaymentDetails.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Purchase/PaymentDetails.cs
@@ -1,13 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
-using System.Text.Json.Serialization;
-using NJsonSchema.Converters;
namespace CoffeeCard.Models.DataTransferObjects.v2.Purchase
{
[KnownType(typeof(MobilePayPaymentDetails))]
[KnownType(typeof(FreePurchasePaymentDetails))]
- [JsonConverter(typeof(JsonInheritanceConverter))]
public abstract class PaymentDetails
{
///
diff --git a/coffeecard/CoffeeCard.Tests.Common/BuilderForAttribute.cs b/coffeecard/CoffeeCard.Tests.Common/BuilderForAttribute.cs
new file mode 100644
index 00000000..381492a7
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/BuilderForAttribute.cs
@@ -0,0 +1,17 @@
+namespace CoffeeCard.Tests.Common;
+
+///
+/// Serves as a marker attribute used for source generation
+/// The Type given, will have builder methods generated for it
+/// The methods will be in the format WithPropertyName and allow for fluent api style configuration
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+sealed class BuilderForAttribute : Attribute
+{
+ public Type EntityType { get; }
+
+ public BuilderForAttribute(Type entityType)
+ {
+ EntityType = entityType;
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/BaseBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/BaseBuilder.cs
new file mode 100644
index 00000000..2762820a
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/BaseBuilder.cs
@@ -0,0 +1,48 @@
+using AutoBogus;
+using AutoBogus.Conventions;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ public abstract class BaseBuilder where T : class
+ {
+ protected readonly AutoFaker Faker = new();
+ protected BaseBuilder()
+ {
+ Faker.Configure(builder => builder.WithConventions());
+ }
+
+ ///
+ /// Creates a new instance of type T,
+ /// using the configuration set by using the builderMethods
+ ///
+ public T Build()
+ {
+ return Faker.Generate();
+ }
+
+ ///
+ /// Creates a new list of type T,
+ /// using the configuration set by using the builderMethods
+ ///
+ public List Build(int count)
+ {
+ return Faker.Generate(count);
+ }
+ }
+
+ public interface IBuilder where T : class
+ {
+ ///
+ /// Gives a standard configured builder,
+ /// where lists of linked entities are empty
+ /// and nullable enities are null
+ ///
+ public static abstract T Simple();
+
+ ///
+ /// Gives a standard configured builder,
+ /// where all linked entities are populated
+ ///
+ public static abstract T Typical();
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/ProductBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/ProductBuilder.cs
new file mode 100644
index 00000000..50db3e93
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/ProductBuilder.cs
@@ -0,0 +1,22 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Product))]
+ public partial class ProductBuilder
+ {
+ public static ProductBuilder Simple()
+ {
+ return new ProductBuilder()
+ .WithName(f => f.Commerce.ProductName())
+ .WithDescription(f => f.Commerce.ProductDescription())
+ .WithNumberOfTickets(f => f.PickRandom(1, 10))
+ .WithProductUserGroup(new List());
+ }
+
+ public static ProductBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/ProductUserGroupBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/ProductUserGroupBuilder.cs
new file mode 100644
index 00000000..91011ab0
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/ProductUserGroupBuilder.cs
@@ -0,0 +1,22 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(ProductUserGroup))]
+ public partial class ProductUserGroupBuilder
+ {
+ public static ProductUserGroupBuilder Simple()
+ {
+ var product = ProductBuilder.Simple().Build();
+ return new ProductUserGroupBuilder()
+ .WithProduct(product)
+ .WithUserGroup(f =>
+ f.PickRandom(UserGroup.Barista, UserGroup.Board, UserGroup.Customer, UserGroup.Manager));
+ }
+
+ public static ProductUserGroupBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/ProgrammeBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/ProgrammeBuilder.cs
new file mode 100644
index 00000000..bda9291d
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/ProgrammeBuilder.cs
@@ -0,0 +1,21 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Programme))]
+ public partial class ProgrammeBuilder
+ {
+ public static ProgrammeBuilder Simple()
+ {
+ return new ProgrammeBuilder()
+ .WithUsers(new List())
+ .WithShortName(f => f.Random.String2(3))
+ .WithFullName(f => f.Commerce.Department());
+ }
+
+ public static ProgrammeBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/PurchaseBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/PurchaseBuilder.cs
new file mode 100644
index 00000000..783fcdd5
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/PurchaseBuilder.cs
@@ -0,0 +1,23 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Purchase))]
+ public partial class PurchaseBuilder
+ {
+ public static PurchaseBuilder Simple()
+ {
+ var purchasedBy = UserBuilder.Simple().Build();
+ return new PurchaseBuilder().WithPurchasedBy(purchasedBy)
+ .WithProductName(f => f.Commerce.ProductName())
+ .WithTickets(new List())
+ .WithTickets(new List())
+ .WithNumberOfTickets(0);
+ }
+
+ public static PurchaseBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/StatisticBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/StatisticBuilder.cs
new file mode 100644
index 00000000..5fa02625
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/StatisticBuilder.cs
@@ -0,0 +1,20 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Statistic))]
+ public partial class StatisticBuilder
+ {
+ public static StatisticBuilder Simple()
+ {
+ var user = UserBuilder.Simple().Build();
+ return new StatisticBuilder()
+ .WithUser(user);
+ }
+
+ public static StatisticBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/TicketBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/TicketBuilder.cs
new file mode 100644
index 00000000..716835ca
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/TicketBuilder.cs
@@ -0,0 +1,22 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Ticket))]
+ public partial class TicketBuilder
+ {
+ public static TicketBuilder Simple()
+ {
+ var owner = UserBuilder.Simple().Build();
+ var purchase = PurchaseBuilder.Simple().Build();
+ return new TicketBuilder()
+ .WithOwner(owner)
+ .WithPurchase(purchase);
+ }
+
+ public static TicketBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/TokenBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/TokenBuilder.cs
new file mode 100644
index 00000000..fb6d090b
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/TokenBuilder.cs
@@ -0,0 +1,19 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Token))]
+ public partial class TokenBuilder
+ {
+ public static TokenBuilder Simple()
+ {
+ return new TokenBuilder()
+ .WithUser(UserBuilder.Simple().Build());
+ }
+
+ public static TokenBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/UserBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/UserBuilder.cs
new file mode 100644
index 00000000..15b83e69
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/UserBuilder.cs
@@ -0,0 +1,34 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(User))]
+ public partial class UserBuilder
+ {
+ public static UserBuilder Simple()
+ {
+ var programme = ProgrammeBuilder.Simple().Build();
+
+ return new UserBuilder()
+ .WithProgramme(programme)
+ .WithPurchases(new List())
+ .WithStatistics(new List())
+ .WithLoginAttempts(new List())
+ .WithTokens(new List())
+ .WithUserState(UserState.Active)
+ .WithTickets(new List());
+ }
+
+ public static UserBuilder DefaultCustomer()
+ {
+ return Simple()
+ .WithUserGroup(UserGroup.Customer)
+ .WithIsVerified(true);
+ }
+
+ public static UserBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/Builders/VoucherBuilder.cs b/coffeecard/CoffeeCard.Tests.Common/Builders/VoucherBuilder.cs
new file mode 100644
index 00000000..90530e6a
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/Builders/VoucherBuilder.cs
@@ -0,0 +1,21 @@
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Common.Builders
+{
+ [BuilderFor(typeof(Voucher))]
+ public partial class VoucherBuilder
+ {
+ public static VoucherBuilder Simple()
+ {
+ var product = ProductBuilder.Simple().Build();
+ return new VoucherBuilder()
+ .WithProduct(product)
+ .WithUser(f => null);
+ }
+
+ public static VoucherBuilder Typical()
+ {
+ return Simple();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Common/CoffeeCard.Tests.Common.csproj b/coffeecard/CoffeeCard.Tests.Common/CoffeeCard.Tests.Common.csproj
new file mode 100644
index 00000000..69cf9e18
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Common/CoffeeCard.Tests.Common.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ net8.0
+ enable
+ enable
+
+
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Integration/CoffeeCard.Tests.Integration.csproj b/coffeecard/CoffeeCard.Tests.Integration/CoffeeCard.Tests.Integration.csproj
index 723d78cf..243b45ef 100644
--- a/coffeecard/CoffeeCard.Tests.Integration/CoffeeCard.Tests.Integration.csproj
+++ b/coffeecard/CoffeeCard.Tests.Integration/CoffeeCard.Tests.Integration.csproj
@@ -1,8 +1,8 @@
- net6.0
+ net8.0
false
- 8
+ default
81E81CE9-7DDA-4A9B-860B-6A473BC4F06F
@@ -10,19 +10,10 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -30,9 +21,16 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
@@ -42,4 +40,4 @@
-
\ No newline at end of file
+
diff --git a/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/GetAccountTest.cs b/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/GetAccountTest.cs
new file mode 100644
index 00000000..a35c64da
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/GetAccountTest.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using CoffeeCard.Tests.Integration.WebApplication;
+using CoffeeCard.WebApi;
+using Xunit;
+using CoffeeCard.Models.DataTransferObjects.v2.User;
+using CoffeeCard.Tests.Common.Builders;
+using CoffeeCard.Models.Entities;
+
+namespace CoffeeCard.Tests.Integration.Controllers.Account
+{
+
+ public class GetAccountTest : BaseIntegrationTest
+ {
+ private const string GetAccountUrl = "api/v2/account";
+ public GetAccountTest(CustomWebApplicationFactory factory) : base(factory)
+ {
+ }
+
+ [Fact]
+ public async Task Get_account_succeeds_when_authenticated_for_existing_account()
+ {
+ var user = UserBuilder.DefaultCustomer().Build();
+ await Context.Users.AddAsync(user);
+ await Context.SaveChangesAsync();
+ SetDefaultAuthHeader(user);
+
+ var response = await Client.GetAsync(GetAccountUrl);
+ var account = await DeserializeResponseAsync(response);
+
+ Assert.Equal(user.Email, account.Email);
+ Assert.Equal(user.Name, account.Name);
+ Assert.Equal(user.Programme.FullName, account.Programme.FullName);
+ Assert.Equal(user.UserGroup.toUserRole(), account.Role);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/LoginTest.cs b/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/LoginTest.cs
new file mode 100644
index 00000000..191aa661
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Integration/Controllers/Account/LoginTest.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using CoffeeCard.Models.DataTransferObjects.User;
+using CoffeeCard.Tests.Common.Builders;
+using CoffeeCard.Tests.Integration.WebApplication;
+using CoffeeCard.WebApi;
+using Xunit;
+
+namespace CoffeeCard.Tests.Integration.Controllers.Account
+{
+
+ public class LoginTest : BaseIntegrationTest
+ {
+ private const string LoginUrl = "api/v1/account/login";
+ public LoginTest(CustomWebApplicationFactory factory) : base(factory)
+ {
+ }
+
+ [Fact]
+ public async Task Unknown_user_login_fails()
+ {
+ var loginRequest = new LoginDto
+ {
+ Password = "test",
+ Email = "test@email.dk",
+ Version = "2.1.0"
+ };
+
+ var response = await Client.PostAsJsonAsync(LoginUrl, loginRequest);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Known_user_login_succeeds_returns_token()
+ {
+ var user = UserBuilder.DefaultCustomer().Build();
+ var plaintextPassword = user.Password;
+ user.Password = HashPassword(plaintextPassword + user.Salt);
+
+ await Context.Users.AddAsync(user);
+ await Context.SaveChangesAsync();
+
+ var loginRequest = new LoginDto
+ {
+ Password = plaintextPassword,
+ Email = user.Email,
+ Version = "2.1.0"
+ };
+ var response = await Client.PostAsJsonAsync(LoginUrl, loginRequest);
+
+ var token = await DeserializeResponseAsync(response);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.NotEmpty(token.Token!);
+ }
+
+ private static string HashPassword(string password)
+ {
+ var byteArr = Encoding.UTF8.GetBytes(password);
+ using var hasher = SHA256.Create();
+ var hashBytes = hasher.ComputeHash(byteArr);
+ return Convert.ToBase64String(hashBytes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Integration/WebApplication/BaseIntegrationTest.cs b/coffeecard/CoffeeCard.Tests.Integration/WebApplication/BaseIntegrationTest.cs
new file mode 100644
index 00000000..155d71f3
--- /dev/null
+++ b/coffeecard/CoffeeCard.Tests.Integration/WebApplication/BaseIntegrationTest.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Net.Http;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using CoffeeCard.Common.Configuration;
+using CoffeeCard.Library.Persistence;
+using CoffeeCard.Models.Entities;
+using CoffeeCard.WebApi;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.IdentityModel.Tokens;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace CoffeeCard.Tests.Integration.WebApplication
+{
+ [Collection("Integration tests, to be run sequentially")]
+ public abstract class BaseIntegrationTest : CustomWebApplicationFactory, IClassFixture>
+ {
+ private readonly CustomWebApplicationFactory _factory;
+ private readonly IServiceScope _scope;
+ protected readonly HttpClient Client;
+ protected readonly CoffeeCardContext Context;
+
+ protected BaseIntegrationTest(CustomWebApplicationFactory factory)
+ {
+ // Set the random seed used for generation of data in the builders
+ // This ensures our tests are deterministic within a specific version of the code
+ var seed = new Random(42);
+ Bogus.Randomizer.Seed = seed;
+ _factory = factory;
+ _scope = _factory.Services.CreateScope();
+
+ Client = GetHttpClient();
+ Context = GetCoffeeCardContext();
+ }
+
+ private HttpClient GetHttpClient()
+ {
+ var client = CreateClient();
+
+ return client;
+ }
+
+ protected void SetDefaultAuthHeader(User user)
+ {
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.Email, user.Email),
+ new Claim(ClaimTypes.Name, user.Name),
+ new Claim("UserId", user.Id.ToString()),
+ new Claim(ClaimTypes.Role, user.UserGroup.ToString())
+ };
+ var token = GenerateToken(claims);
+ Client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", token);
+ }
+
+ private string GenerateToken(IEnumerable claims)
+ {
+ var scopedServices = _scope.ServiceProvider;
+ var identitySettings = scopedServices.GetRequiredService();
+ var key = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(identitySettings.TokenKey)); // get token from appsettings.json
+
+ var jwt = new JwtSecurityToken("AnalogIO",
+ "Everyone",
+ claims,
+ DateTime.UtcNow,
+ DateTime.UtcNow.AddHours(24),
+ new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
+ );
+
+ return new JwtSecurityTokenHandler().WriteToken(jwt); //the method is called WriteToken but returns a string
+ }
+
+ protected void RemoveRequestHeaders()
+ {
+ Client.DefaultRequestHeaders.Clear();
+ }
+
+ private CoffeeCardContext GetCoffeeCardContext()
+ {
+ // Create a scope to obtain a reference to the database context (ApplicationDbContext).
+ var scopedServices = _scope.ServiceProvider;
+ var context = scopedServices.GetRequiredService();
+
+ // Ensure the database is cleaned for each test run
+ context.Database.EnsureDeleted();
+ context.Database.EnsureCreated();
+
+ return context;
+ }
+
+ ///
+ /// Helper method to deserialize a response from the api
+ ///
+ protected static async Task DeserializeResponseAsync(HttpResponseMessage response)
+ {
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStringAsync();
+ return JsonConvert.DeserializeObject(content);
+ }
+
+ public override ValueTask DisposeAsync()
+ {
+ _scope.Dispose();
+ return base.DisposeAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/coffeecard/CoffeeCard.Tests.Integration/WebApplication/CustomWebApplicationFactory.cs b/coffeecard/CoffeeCard.Tests.Integration/WebApplication/CustomWebApplicationFactory.cs
index c7c272f1..8037cf50 100644
--- a/coffeecard/CoffeeCard.Tests.Integration/WebApplication/CustomWebApplicationFactory.cs
+++ b/coffeecard/CoffeeCard.Tests.Integration/WebApplication/CustomWebApplicationFactory.cs
@@ -1,3 +1,4 @@
+using System;
using System.Linq;
using CoffeeCard.Common.Configuration;
using CoffeeCard.Library.Persistence;
@@ -51,13 +52,6 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
// Build the service provider
var sp = services.BuildServiceProvider();
- // Create a scope to obtain a reference to the database context (ApplicationDbContext).
- using var scope = sp.CreateScope();
- var scopedServices = scope.ServiceProvider;
- var db = scopedServices.GetRequiredService();
-
- // Ensure the database is created.
- db.Database.EnsureCreated();
});
}
}
diff --git a/coffeecard/CoffeeCard.Tests.Unit/CoffeeCard.Tests.Unit.csproj b/coffeecard/CoffeeCard.Tests.Unit/CoffeeCard.Tests.Unit.csproj
index 9e7c3edc..05587902 100644
--- a/coffeecard/CoffeeCard.Tests.Unit/CoffeeCard.Tests.Unit.csproj
+++ b/coffeecard/CoffeeCard.Tests.Unit/CoffeeCard.Tests.Unit.csproj
@@ -1,8 +1,8 @@
- net6.0
+ net8.0
false
- 8
+ default
24274C8F-678F-4E4D-98A0-2899163BCCBA
@@ -10,7 +10,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
@@ -22,9 +21,10 @@
all
runtime; build; native; contentfiles; analyzers
+
-
\ No newline at end of file
+
diff --git a/coffeecard/CoffeeCard.Tests.Unit/Services/AccountServiceTest.cs b/coffeecard/CoffeeCard.Tests.Unit/Services/AccountServiceTest.cs
index 34d49b41..d3ca0612 100644
--- a/coffeecard/CoffeeCard.Tests.Unit/Services/AccountServiceTest.cs
+++ b/coffeecard/CoffeeCard.Tests.Unit/Services/AccountServiceTest.cs
@@ -632,7 +632,7 @@ public async Task VerifyRegistrationReturnsTrueGivenValidToken()
};
var identitySettings = new IdentitySettings
{
- TokenKey = "This is a long test token key"
+ TokenKey = "SuperLongSigningKeySuperLongSigningKey"
};
var tokenService = new TokenService(identitySettings, new ClaimsUtilities(context));
@@ -697,7 +697,7 @@ private static string WriteTokenString(IEnumerable claims)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = new SymmetricSecurityKey(
- Encoding.UTF8.GetBytes("This is a long test token key"));
+ Encoding.UTF8.GetBytes("SuperLongSigningKeySuperLongSigningKey"));
var jwt = new JwtSecurityToken("AnalogIO",
"Everyone",
diff --git a/coffeecard/CoffeeCard.Tests.Unit/Services/TokenServiceTest.cs b/coffeecard/CoffeeCard.Tests.Unit/Services/TokenServiceTest.cs
index ef628af9..3c9ddb31 100644
--- a/coffeecard/CoffeeCard.Tests.Unit/Services/TokenServiceTest.cs
+++ b/coffeecard/CoffeeCard.Tests.Unit/Services/TokenServiceTest.cs
@@ -22,7 +22,7 @@ public TokenServiceTest()
_identity = new IdentitySettings();
//creates the key for signing the token
- const string keyForHmacSha256 = "signingKey";
+ const string keyForHmacSha256 = "SuperLongSigningKey";
_identity.TokenKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(keyForHmacSha256)).ToString();
}
@@ -99,7 +99,7 @@ public async Task ValidateTokenGivenInvalidSignedTokenReturnsFalse()
await using (context)
{
var key = new SymmetricSecurityKey(
- Encoding.UTF8.GetBytes("Invalid signing key"));
+ Encoding.UTF8.GetBytes("Super long invalid signing key, longer than 256bytes"));
var jwt = new JwtSecurityToken("AnalogIO",
"Everyone",
@@ -213,4 +213,4 @@ private CoffeeCardContext GenerateCoffeeCardContext(string uniqueString)
return new CoffeeCardContext(builder.Options, databaseSettings, environmentSettings);
}
}
-}
\ No newline at end of file
+}
diff --git a/coffeecard/CoffeeCard.WebApi/CoffeeCard.WebApi.csproj b/coffeecard/CoffeeCard.WebApi/CoffeeCard.WebApi.csproj
index 49798d10..5eebbc7e 100644
--- a/coffeecard/CoffeeCard.WebApi/CoffeeCard.WebApi.csproj
+++ b/coffeecard/CoffeeCard.WebApi/CoffeeCard.WebApi.csproj
@@ -1,8 +1,8 @@
- net6.0
+ net8.0
InProcess
- 8
+ default
5562C898-513F-493A-A335-417C39476714
d3bbf778-c5fe-4240-b2ca-dfa64082cc0e
true
@@ -30,29 +30,25 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
@@ -62,4 +58,4 @@
-
\ No newline at end of file
+
diff --git a/coffeecard/CoffeeCard.WebApi/Dockerfile b/coffeecard/CoffeeCard.WebApi/Dockerfile
index 63d1ca9e..945d0878 100644
--- a/coffeecard/CoffeeCard.WebApi/Dockerfile
+++ b/coffeecard/CoffeeCard.WebApi/Dockerfile
@@ -1,11 +1,11 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
-FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
-FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["CoffeeCard.WebApi/CoffeeCard.WebApi.csproj", "CoffeeCard.WebApi/"]
COPY ["CoffeeCard.Common/CoffeeCard.Common.csproj", "CoffeeCard.Common/"]
diff --git a/coffeecard/CoffeeCard.WebApi/Startup.cs b/coffeecard/CoffeeCard.WebApi/Startup.cs
index dc0aaa9e..ad1cffa8 100644
--- a/coffeecard/CoffeeCard.WebApi/Startup.cs
+++ b/coffeecard/CoffeeCard.WebApi/Startup.cs
@@ -241,7 +241,7 @@ private static void GenerateOpenApiDocument(IServiceCollection services)
config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("apikey"));
// Assume not null as default unless parameter is marked as nullable
- config.DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull;
+ // config.DefaultReferenceTypeNullHandling = ReferenceTypeNullHandling.NotNull;
});
}
}
@@ -260,7 +260,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVers
app.UseHsts();
app.UseOpenApi();
- app.UseSwaggerUi3();
+ app.UseSwaggerUi();
app.UseHttpsRedirection();
@@ -289,4 +289,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVers
}
}
#pragma warning restore CS1591
-}
\ No newline at end of file
+}
diff --git a/coffeecard/CoffeeCard.WebApi/appsettings.json b/coffeecard/CoffeeCard.WebApi/appsettings.json
index a5aaf876..5f78c643 100644
--- a/coffeecard/CoffeeCard.WebApi/appsettings.json
+++ b/coffeecard/CoffeeCard.WebApi/appsettings.json
@@ -13,7 +13,7 @@
"SchemaName": "dbo"
},
"IdentitySettings": {
- "TokenKey": "local-development-token",
+ "TokenKey": "super-long-local-development-token",
"AdminToken": "local-development-admintoken",
"ApiKey": "local-development-apikey"
},
@@ -43,7 +43,7 @@
"Default": "Information",
"Override": {
"System": "Information",
- "Microsoft": "Warning"
+ "Microsoft": "Information"
}
},
"WriteTo": [
diff --git a/coffeecard/CoffeeCardAPI.sln b/coffeecard/CoffeeCardAPI.sln
index c3660e14..1ae693c7 100644
--- a/coffeecard/CoffeeCardAPI.sln
+++ b/coffeecard/CoffeeCardAPI.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29806.167
@@ -13,11 +13,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.MobilePay", "Cof
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.Common", "CoffeeCard.Common\CoffeeCard.Common.csproj", "{2CCAF827-E341-48FC-ABCE-7EDFA3E84E65}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoffeeCard.Library", "CoffeeCard.Library\CoffeeCard.Library.csproj", "{F05C6E90-C02D-43BF-8BBB-615785C465DF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.Library", "CoffeeCard.Library\CoffeeCard.Library.csproj", "{F05C6E90-C02D-43BF-8BBB-615785C465DF}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoffeeCard.Models", "CoffeeCard.Models\CoffeeCard.Models.csproj", "{0BF48733-C3DF-4006-BD25-F5340710B5FA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.Models", "CoffeeCard.Models\CoffeeCard.Models.csproj", "{0BF48733-C3DF-4006-BD25-F5340710B5FA}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoffeeCard.MobilePay.GenerateApi", "CoffeeCard.MobilePay.GenerateApi\CoffeeCard.MobilePay.GenerateApi.csproj", "{D295CCD7-DDB7-477D-82B6-53AFA97F5749}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.MobilePay.GenerateApi", "CoffeeCard.MobilePay.GenerateApi\CoffeeCard.MobilePay.GenerateApi.csproj", "{D295CCD7-DDB7-477D-82B6-53AFA97F5749}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoffeeCard.Common.Test", "CoffeeCard.Tests.Common\CoffeeCard.Tests.Common.csproj", "{938E0AD9-C1C8-4C10-B9FB-DD279DAD4D0B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoffeeCard.Generators", "CoffeeCard.Generators\CoffeeCard.Generators.csproj", "{BE332D7E-FD07-4878-B7E3-A06988F0C382}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -57,6 +61,14 @@ Global
{D295CCD7-DDB7-477D-82B6-53AFA97F5749}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D295CCD7-DDB7-477D-82B6-53AFA97F5749}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D295CCD7-DDB7-477D-82B6-53AFA97F5749}.Release|Any CPU.Build.0 = Release|Any CPU
+ {938E0AD9-C1C8-4C10-B9FB-DD279DAD4D0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {938E0AD9-C1C8-4C10-B9FB-DD279DAD4D0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {938E0AD9-C1C8-4C10-B9FB-DD279DAD4D0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {938E0AD9-C1C8-4C10-B9FB-DD279DAD4D0B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BE332D7E-FD07-4878-B7E3-A06988F0C382}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BE332D7E-FD07-4878-B7E3-A06988F0C382}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BE332D7E-FD07-4878-B7E3-A06988F0C382}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BE332D7E-FD07-4878-B7E3-A06988F0C382}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/coffeecard/global.json b/coffeecard/global.json
index 6fbe9d02..18b689d1 100644
--- a/coffeecard/global.json
+++ b/coffeecard/global.json
@@ -1,7 +1,7 @@
{
"sdk": {
- "version": "6.0.0",
- "rollForward": "latestMajor",
- "allowPrerelease": true
+ "version": "8.0.0",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
}
}