Unit testing that the swagger doc is correct without starting a server - swashbuckle.aspnetcore

I'd like to test that the swagger document is correct for my application (mainly, because I've added a strategy to generate custom OperationIds and I want to ensure they are correctly unique)
However, the only solutions I found are all using a "real" server (cf https://stackoverflow.com/a/52521454/1545567), which is not an option for me since I do not have the database, message bus, etc... when I launch the unit tests in CI...
At the moment, I have the following but it always generate 0 paths and 0 models ...
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using SampleCheckIn;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Linq;
using Xunit;
using SampleCheckIn.Def;
using Service.Utils;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.FileProviders;
namespace D4Interop.Tests
{
public class TmpTest
{
[Fact]
public void Tmp()
{
var controllers = typeof(Startup).Assembly.GetTypes().Where(x => IsController(x)).ToList();
controllers.Any().Should().BeTrue();
var services = new ServiceCollection();
controllers.ForEach(c => services.AddScoped(c));
services.AddLogging(logging => logging.AddConsole());
services.AddControllers(); //here, I've also tried AddMvcCore and other ASP methods...
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("api", new OpenApiInfo { Title = Constants.SERVICE_NAME, Version = "_", Description = Constants.SERVICE_DESC });
//c.OperationFilter<SwaggerUniqueOperationId>(); //this is my filter that ensures the operationId is unique
c.CustomOperationIds(apiDesc =>
{
return apiDesc.TryGetMethodInfo(out var methodInfo) ? methodInfo.Name : null;
});
});
services.AddSingleton<IWebHostEnvironment>(new FakeWebHostEnvironment());
var serviceProvider = services.BuildServiceProvider();
var swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
var swagger = swaggerProvider.GetSwagger("api");
swagger.Should().NotBeNull();
swagger.Paths.Any().Should().BeTrue();
}
private bool IsController(Type x)
{
return typeof(Microsoft.AspNetCore.Mvc.ControllerBase).IsAssignableFrom(x);
}
}
internal class FakeWebHostEnvironment : IWebHostEnvironment
{
public FakeWebHostEnvironment()
{
}
public IFileProvider WebRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public string WebRootPath { get => "/root"; set => throw new NotImplementedException(); }
public string EnvironmentName { get => "dev"; set => throw new NotImplementedException(); }
public string ApplicationName { get => "app"; set => throw new NotImplementedException(); }
public string ContentRootPath { get => "/"; set => throw new NotImplementedException(); }
public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
}
}

Ok, I've finally found that I just need to mix the linked answer with my code :
[Fact]
public async Task TestSwagger()
{
var server = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(options => { options.UseStartup<Startup>(); })
.Build();
var swagger = server.Services
.GetRequiredService<ISwaggerProvider>()
.GetSwagger("xxx"); //xxx should be the name of your API
swagger.Should().NotBeNull();
swagger.Paths.Any().Should().BeTrue();
swagger.Components.Schemas.Should().NotBeNull();
}

Related

How to receive the ASPN Token in Xamarin Forms iOS-App

I followed this tutorial to implement Push-Notifications in my Xamarin-Forms App (especially the iOS part). Now my problem is, when I press the register-button, I get the error message "Unable to resolve token for APNS".
Stepping through the code in debug mode I could verify, that the Token property in DeviceInstallationService is indeed null.
So I've gone one step back, and identified that the Token is set only via RegisteredForRemoteNotification in AppDelegate.cs, but this method is never called when I run the App.
Here is some code: App-Delegate
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Foundation;
using Notes.iOS.Extensions;
using Notes.iOS.Services;
using Notes.Services;
using UIKit;
using UserNotifications;
using Xamarin.Essentials;
using System.Collections.Generic;
using System.Linq;
using Syncfusion.SfCalendar.XForms.iOS;
namespace Notes.iOS
{
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
IPushDemoNotificationActionService _notificationActionService;
INotificationRegistrationService _notificationRegistrationService;
IDeviceInstallationService _deviceInstallationService;
IPushDemoNotificationActionService NotificationActionService
=> _notificationActionService ??
(_notificationActionService =
ServiceContainer.Resolve<IPushDemoNotificationActionService>());
INotificationRegistrationService NotificationRegistrationService
=> _notificationRegistrationService ??
(_notificationRegistrationService =
ServiceContainer.Resolve<INotificationRegistrationService>());
IDeviceInstallationService DeviceInstallationService
=> _deviceInstallationService ??
(_deviceInstallationService =
ServiceContainer.Resolve<IDeviceInstallationService>());
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
Bootstrap.Begin(() => new DeviceInstallationService());
if (DeviceInstallationService.NotificationsSupported)
{
UNUserNotificationCenter.Current.RequestAuthorization(
UNAuthorizationOptions.Alert |
UNAuthorizationOptions.Badge |
UNAuthorizationOptions.Sound,
(approvalGranted, error) =>
{
if (approvalGranted && error == null)
RegisterForRemoteNotifications();
});
}
LoadApplication(new App());
using (var userInfo = options?.ObjectForKey(
UIApplication.LaunchOptionsRemoteNotificationKey) as NSDictionary)
ProcessNotificationActions(userInfo);
return base.FinishedLaunching(app, options);
}
void RegisterForRemoteNotifications()
{
MainThread.BeginInvokeOnMainThread(() =>
{
var pushSettings = UIUserNotificationSettings.GetSettingsForTypes(
UIUserNotificationType.Alert |
UIUserNotificationType.Badge |
UIUserNotificationType.Sound,
new NSSet());
UIApplication.SharedApplication.RegisterUserNotificationSettings(pushSettings);
UIApplication.SharedApplication.RegisterForRemoteNotifications();
});
}
Task CompleteRegistrationAsync(NSData deviceToken)
{
DeviceInstallationService.Token = deviceToken.ToHexString();
return NotificationRegistrationService.RefreshRegistrationAsync();
}
void ProcessNotificationActions(NSDictionary userInfo)
{
if (userInfo == null)
return;
try
{
var actionValue = userInfo.ObjectForKey(new NSString("action")) as NSString;
if (!string.IsNullOrWhiteSpace(actionValue?.Description))
NotificationActionService.TriggerAction(actionValue.Description);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
=> CompleteRegistrationAsync(deviceToken).ContinueWith((task)
=> { if (task.IsFaulted) throw task.Exception; });
public override void ReceivedRemoteNotification(
UIApplication application,
NSDictionary userInfo)
=> ProcessNotificationActions(userInfo);
public override void FailedToRegisterForRemoteNotifications(
UIApplication application,
NSError error)
=> Debug.WriteLine(error.Description);
}
}
DeviceInstallationService:
using System;
using Notes.Models;
using Notes.Services;
using UIKit;
namespace Notes.iOS.Services
{
public class DeviceInstallationService : IDeviceInstallationService
{
const int SupportedVersionMajor = 13;
const int SupportedVersionMinor = 0;
public string Token { get; set; }
public bool NotificationsSupported
=> UIDevice.CurrentDevice.CheckSystemVersion(SupportedVersionMajor, SupportedVersionMinor);
public string GetDeviceId()
=> UIDevice.CurrentDevice.IdentifierForVendor.ToString();
public DeviceInstallation GetDeviceInstallation(params string[] tags)
{
if (!NotificationsSupported)
throw new Exception(GetNotificationsSupportError());
if (string.IsNullOrWhiteSpace(Token))
throw new Exception("Unable to resolve token for APNS");
var installation = new DeviceInstallation
{
InstallationId = GetDeviceId(),
Platform = "apns",
PushChannel = Token
};
installation.Tags.AddRange(tags);
return installation;
}
string GetNotificationsSupportError()
{
if (!NotificationsSupported)
return $"This app only supports notifications on iOS {SupportedVersionMajor}.{SupportedVersionMinor} and above. You are running {UIDevice.CurrentDevice.SystemVersion}.";
if (Token == null)
return $"This app can support notifications but you must enable this in your settings.";
return "An error occurred preventing the use of push notifications";
}
}
}
As you can see this is really 1:1 the example code, the only difference is that my project is called Notes.
I skipped the Firebase and Android-Part as I only need push-notifications for iOS so far and as far as I underestood these are not necessary for iOS only.
Thanks your help!
Some points to check if RegisteredForRemoteNotification not called:
Open Entitlements.plist and ensure that Enable Push Notifications is checked when viewed in the Entitlements tab. Then, ensure the APS Environment setting is set to development when viewed in the Source tab.
Make sure that you are testing the remote-notification in a real device instead of a simulator. A simulator does not support remote-notification.
Make sure that you agreed receiving notification permission.
Make sure the certification you use has enabled the push notification ability.
Refer: configuring-the-remote-notifications-environment
You can look at the message returned from the following function in your App Delegate AppDelegate.cs
public override void FailedToRegisterForRemoteNotifications(
UIApplication application,
NSError error)
For instance
no valid “aps-environment” entitlement string found for application

Shopware 6: Extend existing core DAL entities with a reference

I'm trying to create a connection between 2 existing entities PropertyGroup and CustomFieldSet. Use-case is irrelevant.
So I created an EntityExtension:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
public function getDefinitionClass(): string
{
return PropertyGroupDefinition::class;
}
And override the administration component to also include this association when loading the entity:
Component.override('sw-property-detail', {
methods: {
loadEntityData() {
this.isLoading = true;
const criteria = this.defaultCriteria;
criteria.addAssociation('customFieldSet', new Criteria(1, 500));
this.propertyRepository.get(this.groupId, Shopware.Context.api, criteria)
.then((currentGroup) => {
this.propertyGroup = currentGroup;
this.isLoading = false;
}).catch(() => {
this.isLoading = false;
});
}
}
});
(I tried to override defaultCriteria but that didn't work because of this.$super being unable to access computed properties).
But it keeps saying FRAMEWORK__ASSOCIATION_NOT_FOUND. I debugged the EntityDefinition and it seems that this extension is not even loaded.
I checked if my EntityExtension is loaded in the Symfony container and it is, but it seems that it doesn't reach the entity definition.
The EntityExtension seems to be missing the addition of a FkField inside the function extendFields:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('custom_field_set', 'customFieldSetId', CustomFieldSetDefinition::class)),
);
$collection->add(
(new ManyToOneAssociationField('customFieldSet', 'custom_field_set', CustomFieldSetDefinition::class))
);
}
A new use statement has to be added for the FkField:
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;

Configure AutoMapper using LightInject

Does anyone know how to configure AutoMapper using LightInject? The AutoMapper documentation only has examples for Ninject and Simple Injector.
I am having difficulty trying to register the AutoMapper configuration.
I'm using ASP.NET MVC C#.
public class CompositionRoot : ICompositionRoot
{
public void Compose(IServiceRegistry serviceRegistry)
{
serviceRegistry.Register(c => new AutoMapperConfiguration());
}
}
public static class AutoMapperConfiguration
{
public AutoMapperConfiguration()
{
Mapper.Initialize(cfg =>
cfg.AddProfiles(typeof(Namespace.Class).Assembly)
);
}
}
I figured it out. The code below is in the CompositionRoot, where the factory is registered using IServiceRegistry. I will be moving the var config = new MapperConfiguration(cfg => cfg.AddProfiles(typeof(CustomProfileClass).Assembly)); code to a custom MapperConfiguration class that I will create.
public class CompositionRoot : ICompositionRoot
{
public void Compose(IServiceRegistry serviceRegistry)
{
var config = new MapperConfiguration(cfg => cfg.AddProfiles(typeof(CustomProfileClass)));
serviceRegistry.Register(c => config.CreateMapper());
}
}

How to MOQ a repository and ensure that the method is called using VerifyAll()?

This is the repository class
using System;
using System.Collections.Generic;
using Dealer.Rails.Common.Utils;
using Dealer.Rails.Repository.Entities.C3;
using Dealer.Rails.Repository.Repositories.C3.Interfaces;
using Dealer.Rails.Repository.Repositories.Soar;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Dealer.Rails.Repository.Repositories.C3
{
public class VehicleStagingRepository : BaseRepository, IVehicleStagingRepository
{
public VehicleStagingRepository(ILogger<VehicleStagingRepository> logger)
: base(logger)
{
}
public void SaveVehicleStaging(List<ImportStageVh> vehicleStagingRecords, C3Context c3Context)
{
using (var transaction = c3Context.Database.BeginTransaction())
{
var vehicleStagingRecordBeforeSaving = new ImportStageVh();
try
{
foreach (var vehicleStagingRecord in vehicleStagingRecords)
{
vehicleStagingRecordBeforeSaving = vehicleStagingRecord;
c3Context.ImportStageVh.Add(vehicleStagingRecord);
c3Context.SaveChanges();
}
}
catch (TimeoutException tex)
{
transaction.Rollback();
_logger.LogError(LoggingEvents.SaveEmployeeStaging, tex.Message);
throw;
}
catch (Exception ex)
{
transaction.Rollback();
_logger.LogError(LoggingEvents.SaveEmployeeStaging, ex, JsonConvert.SerializeObject(vehicleStagingRecordBeforeSaving));
throw new Exception(vehicleStagingRecordBeforeSaving.ToString(),ex);
}
transaction.Commit();
}
}
}
}
This is the Xunit Test Cases using MOQ.
using Dealer.Rails.Repository.Entities.C3;
using Dealer.Rails.Repository.Repositories.C3;
using Dealer.Rails.Repository.Repositories.C3.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using System.Collections.Generic;
using Xunit;
namespace Dealer.Rails.Repository.Tests.Repositories.Rails
{
public class VehicleStagingRepositoryTests
{
[Fact]
[Trait("Repositories", "C3")]
public void Should_Dump_Records_To_Vehicle_Staging_Dump()
{
// Arrange
var optionsBuilder = new DbContextOptionsBuilder<C3Context>();
optionsBuilder.UseSqlServer("Data Source=dev.c3.dealertrack.corp;Initial Catalog=c3_bmc;Integrated Security=True;MultipleActiveResultSets=True;Application Name=EntityFramework");
var mockLogger = new Mock<ILogger<VehicleStagingRepository>>();
var mockC3Context = new C3Context(optionsBuilder.Options);
// var vehicleStagingRecords = new List<ImportStageVh>();
var mockRepository = new Mock<IVehicleStagingRepository>();
mockRepository.Setup(x => x.SaveVehicleStaging(It.IsAny<List<ImportStageVh>>(), mockC3Context));
// System under test is TemplateColumnRepository
var sut = new VehicleStagingRepository(mockLogger.Object);
// Act
sut.SaveVehicleStaging(new List<ImportStageVh>(), mockC3Context);
// Assert
mockRepository.Verify();
// mockRepository.Verify(m=>m.SaveVehicleStaging(vehicleStagingRecords,mockC3Context),Times.Once,"Failed to call SaveVehicleStaging method");
}
}
}
MOQ should allow me to independently test the repository. The issues that I am facing are
In the line optionsBuilder.UseSqlServer.. If use optionsBuilder.UseSqlServer("Some dummy connection string"); The code throws an exception. It my opinion it should not.
In the line mockRepository.Verify();, If use mockRepository.VerifyAll(), it fails at line
3. Even the next commented line is throwing error. In my opinions it should have executed SaveStagingVehicle method atleast once.
Please guide me in resolving these issues.

ASP.NET Core 1 Web API Model Binding Array

How do you model bind an array from the URI with GET in ASP.NET Core 1 Web API (implicitly or explicitly)?
In ASP.NET Web API pre Core 1, this worked:
[HttpGet]
public void Method([FromUri] IEnumerable<int> ints) { ... }
How do you do this in ASP.NET Web API Core 1 (aka ASP.NET 5 aka ASP.NET vNext)? The docs have nothing.
The FromUriAttribute class combines the FromRouteAttribute and FromQueryAttribute classes. Depending the configuration of your routes / the request being sent, you should be able to replace your attribute with one of those.
However, there is a shim available which will give you the FromUriAttribute class. Install the "Microsoft.AspNet.Mvc.WebApiCompatShim" NuGet package through the package explorer, or add it directly to your project.json file:
"dependencies": {
"Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}
While it is a little old, I've found that this article does a pretty good job of explaining some of the changes.
Binding
If you're looking to bind comma separated values for the array ("/api/values?ints=1,2,3"), you will need a custom binder just as before. This is an adapted version of Mrchief's solution for use in ASP.NET Core.
public class CommaDelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
var key = bindingContext.ModelName;
var value = bindingContext.ValueProvider.GetValue(key).ToString();
if (!string.IsNullOrWhiteSpace(value))
{
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
var converter = TypeDescriptor.GetConverter(elementType);
var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => converter.ConvertFromString(x.Trim()))
.ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
values.CopyTo(typedValues, 0);
bindingContext.Result = ModelBindingResult.Success(typedValues);
}
else
{
// change this line to null if you prefer nulls to empty arrays
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0));
}
return TaskCache.CompletedTask;
}
return TaskCache.CompletedTask;
}
}
You can either specify the model binder to be used for all collections in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc().AddMvcOptions(opts =>
{
opts.ModelBinders.Insert(0, new CommaDelimitedArrayModelBinder());
});
}
Or specify it once in your API call:
[HttpGet]
public void Method([ModelBinder(BinderType = typeof(CommaDelimitedArrayModelBinder))] IEnumerable<int> ints)
ASP.NET Core 1.1 Answer
#WillRay's answer is a little outdated. I have written an 'IModelBinder' and 'IModelBinderProvider'. The first can be used with the [ModelBinder(BinderType = typeof(DelimitedArrayModelBinder))] attribute, while the second can be used to apply the model binder globally as I've show below.
.AddMvc(options =>
{
// Add to global model binders so you don't need to use the [ModelBinder] attribute.
var arrayModelBinderProvider = options.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
options.ModelBinderProviders.Insert(
options.ModelBinderProviders.IndexOf(arrayModelBinderProvider),
new DelimitedArrayModelBinderProvider());
})
public class DelimitedArrayModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType)
{
return new DelimitedArrayModelBinder();
}
return null;
}
}
public class DelimitedArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
var values = valueProviderResult
.ToString()
.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
if (values.Length == 0)
{
bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0));
}
else
{
var converter = TypeDescriptor.GetConverter(elementType);
var typedArray = Array.CreateInstance(elementType, values.Length);
try
{
for (int i = 0; i < values.Length; ++i)
{
var value = values[i];
var convertedValue = converter.ConvertFromString(value);
typedArray.SetValue(convertedValue, i);
}
}
catch (Exception exception)
{
bindingContext.ModelState.TryAddModelError(
modelName,
exception,
bindingContext.ModelMetadata);
}
bindingContext.Result = ModelBindingResult.Success(typedArray);
}
return Task.CompletedTask;
}
}
There are some changes in the .NET Core 3.
Microsoft has split out the functionality from the AddMvc method (source).
As AddMvc also includes support for View Controllers, Razor Views and etc. If you don't need to use them in your project (like in an API), you might consider using services.AddControllers() which is for Web API controllers.
So, updated code will look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddMvcOptions(opt =>
{
var mbp = opt.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
opt.ModelBinderProviders.Insert(opt.ModelBinderProviders.IndexOf(mbp), new DelimitedArrayModelBinderProvider());
});
}

Resources