I am trying integration tests with XUnit. I can access the endpoints that have been assigned on MapGet but to not actions inside controllers. I get a NotFound error. I am running the application on an API project with ApplicationFactory : WebApplicationFactory but all requests that I make with Factory.Client return NotFound. Is there any definition needed in integration tests in order to be able to send requests to controllers?
Test projects code:
private ApplicationFactory Factory { get; }
public AccountsControllerTests(ITestOutputHelper output)
{
Factory = new ApplicationFactory(output);
}
[Fact]
public async Task ListBalancesTest()
{
var client = Factory.CreateClient(new WebApplicationFactoryClientOptions(){AllowAutoRedirect = false});;
var resp = await client.GetAsync($"api/v1/test/get");
//!!!!!!! resp.StatusCode is HttpStatusCode.NotFound !!!!!!!
var mapgetResp= await client.GetAsync($"/test");
//!!!!!!! mapgetResp is Working !!!!!!!
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
API Code:
[ApiController]
[Route("api/v1/test")]
public class TestController : ControllerBase
{
[HttpGet("get")]
public async Task<ActionResult> Get()
{
return await Task.FromResult(Ok("Response from test"));
}
}
ApplicationFactory Code:
public class ApplicationFactory : WebApplicationFactory<TestStartup>
{
public ApplicationFactory(ITestOutputHelper testOutput = null)
{
_testOutput = testOutput;
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
var builder = WebHost
.CreateDefaultBuilder()
.UseEnvironment("Development")
.UseStartup<TestStartup>()
.UseSerilog();
if (_testOutput != null)
{
builder = builder.ConfigureLogging(logging =>
{
logging.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider>(new TestOutputHelperLoggerProvider(_testOutput)));
});
}
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseContentRoot(".");
builder.ConfigureServices(services =>
{/...../}
}
}
Startup:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/test", async context =>
{
await context.Response.WriteAsync(#$"Test value from MapGet");
});
/..../
});
Add code in API project in ConfigureWebHost(IWebHostBuilder builder)
builder.UseEnvironment("Test");
builder.ConfigureAppConfiguration((ctx, b) =>
{
ctx.HostingEnvironment.ApplicationName = typeof(Program).Assembly.GetName().Name;
});
builder.ConfigureServices(services =>
{
services.AddMvc()
.AddApplicationPart(typeof(TestStartup).Assembly); //TestStartup is Test project's startup!!
}
And works now thanks!
Related
I am trying to setup tests for my controller endpoints and I came up with this design after some research.
Controller interface
public interface IAuthenticationController
{
Task<IActionResult> Login(AuthenticateUserDTO loginDto);
}
The interface is implemented in the controller class, that inherits ControllerBase and has [ApiController] attribute.
BaseFixture.cs
public class BaseFixture : WebApplicationFactory<TestStartup>
{
private Lazy<HttpClient> client;
public BaseFixture()
{
client = new Lazy<HttpClient>(() => Server.CreateClient());
var repo = Server.Host.Services.GetRequiredService<IUserGestairRepository>();
var task = repo.UserManager.CreateAsync(new User_Gestair()
{
UserName = "dev#dev.com.br",
Email = "dev#dev.com.br"
}, "123456");
task.Wait();
var result = task.Result;
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
return WebHost.CreateDefaultBuilder()
.UseStartup<TestStartup>();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<GestairDbContext>));
services.Remove(descriptor);
services.AddDbContext<GestairDbContext>(options =>
{
options.UseInMemoryDatabase(Guid.NewGuid().ToString());
});
services.AddDefaultIdentity<User_Gestair>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.User.RequireUniqueEmail = true;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
})
.AddRoles<IdentityRole>()
.AddErrorDescriber<IdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddEntityFrameworkStores<GestairDbContext>();
services.AddHttpContextAccessor();
services.AddHttpClient();
services.AddTransient<IAspNetUser, AspNetUser>();
services.AddMediatorConfiguration(typeof(CreateCompanyCommandHandler).Assembly);
services.AddAutoMapper(typeof(Startup));
services.AddScoped<ICompanyController, CompanyController>();
services.AddScoped<IAuthenticationController, AuthenticationController>();
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<IUserGestairRepository, UserGestairRepository>();
services.AddControllers()
.AddApplicationPart(Assembly.Load("Gestair.API"))
.AddControllersAsServices()
.AddFluentValidation(options =>
{
options.RegisterValidatorsFromAssemblyContaining<ApplicationValidations>(lifetime: ServiceLifetime.Scoped);
options.ValidatorFactoryType = typeof(HttpContextServiceProviderValidatorFactory);
});
});
builder.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
}
}
Then, I created the fixture to thest the Authentication controller, for instance.
public class AuthenticationControllerFixture : BaseFixture
{
public IAuthenticationController AuthenticationControllerService;
public readonly HttpClient _client;
public readonly BaseFixture _fixture;
public AuthenticationControllerFixture()
{
AuthenticationControllerService = Server.Host.Services.GetRequiredService<IAuthenticationController>();
}
public AuthenticateUserDTO CriarUsuarioLogin()
{
return new AuthenticateUserDTO()
{
Email = "dev#dev.com.br",
Senha = "123456"
};
}
}
And then I call the controller function in the test itself:
public class AuthenticationControllerTest : IClassFixture<AuthenticationControllerFixture>
{
private readonly AuthenticationControllerFixture _fixture;
public AuthenticationControllerTest(AuthenticationControllerFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task ShoudLogin()
{
var input = _fixture.CriarUsuarioLogin();
var response = await _fixture.AuthenticationControllerService.Login(input);
}
}
While debugging, the call is made, it calls the controller but HttpContext of the controller that implements that interface, is null which is causing error to correctly process the request.
How can I fix this, what am I missing?
I need to make a way for app user to listen to server , and get updated HTTPGET request every time database is updated. Client side implementation is not needed, only server side. I have small experience in SignalR and would appreciate any help on Hub side .
My code so far
Startup
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DataContext>(options =>
{
options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"));
});
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
services.AddScoped<IContactRepository, ContactRepository>();
services.AddControllers();
services.AddSignalR();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Projekt", Version = "v1" });
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Projekt v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<UpdatesHub>("hubs/");
});
} ```
here are my ApiCalls user can use
[ApiController]
[Route("api/[controller]")]
public class ContactsController : ControllerBase
{
private readonly IContactRepository _contactRepository;
private readonly IMapper _mapper;
public ContactsController(IContactRepository contactRepository, IMapper mapper)
{
_contactRepository = contactRepository;
_mapper = mapper;
}
#region API CALLS
[HttpGet]
public async Task<ActionResult<IEnumerable<AppUserDto>>> GetUsers([FromQuery]UserParams userParams)
{
//Ego loading phone numbers, gives circular reference problem,
//use DTOs and Mapping instead
var users = await _contactRepository.GetUsersAsync(userParams);
Response.AddpaginationHeader(users.CurrentPage, users.PageSize,
users.TotalCount, users.TotalPages);
var usersToReturn = _mapper.Map<IEnumerable<AppUserDto>>(users);
return Ok(usersToReturn);
}
//~/api/Contacts/1
[HttpGet("{id}")]
public async Task<ActionResult<AppUser>> GetUser(int id)
{
var user = await _contactRepository.GetUserByIdAsync(id);
var userToReturn = _mapper.Map<AppUserDto>(user);
return Ok(userToReturn);
}
[HttpPost("addContact")]
public async Task<ActionResult> AddContact(AppUser appUser)
{
var IsConstrained = await _contactRepository.CheckIfConstrained(appUser);
if (IsConstrained)
{
return BadRequest("user already exists");
}
else
{
_contactRepository.AddContact(appUser);
var result = await _contactRepository.SaveAllAsync();
if (result)
return Ok();
else
return BadRequest("user not saved to database");
}
}
[HttpPut("update")]
public async Task<ActionResult> Update(AppUser user)
{
//Check if user changed his name, if changed ->check if it's unique then update,
//if name not changed just update
var UserBeforeUpdate = _contactRepository.GetUserByIdAsync(user.Id);
var IsNameChangedBool = _contactRepository.CheckIfNameChanged(user , UserBeforeUpdate.Result);
if (IsNameChangedBool)
{
var IsConstrained = await _contactRepository.CheckIfConstrained(user);
if (IsConstrained)
{
return BadRequest("user already exists");
}
else
{
_contactRepository.Update(user);
if (await _contactRepository.SaveAllAsync()) return Ok("Contact updated");
return BadRequest("user not saved to database");
}
}
else
{
_contactRepository.Update(user);
if (await _contactRepository.SaveAllAsync()) return Ok("Contact updated");
return BadRequest("user not saved to database");
}
} ```
This is Hub part i don't know how to send updated httpget request to user
{
public class UpdatesHub : Hub
{
public async Task SendMessageToCaller()
{
await Clients.Caller.SendAsync("RecieveMessage");
}
}
}
Please, be sure that the route to your hub is correct. For example, if your hub is in the hubs folder you need to add endpoints.MapHub<UpdatesHub>("/hubs/hubName");
Clients can connect to SignalR in your javaScript with:
var connection = new signalR.HubConnectionBuilder().withUrl("/hubs/hubName").build();
The clients can subscribe to messages sent from the server. Something like that:
connection.on("ReceiveMessage", function (message) {
alert(message);
});
Create or modify an overloaded constructor of the class in which the database update function is implemented, in order to access the hub:
...
public class DatabaseClass
{
private IHubContext<UpdatesHub> _hub;
public DatabaseClass(IHubContext<UpdatesHub> hub)
{
_hub = hub;
}
...
Now you can use the hub to send a message to the clients, after updating your database:
...
public class DatabaseClass
{
private IHubContext<UpdatesHub> _hub;
public DatabaseClass(IHubContext<UpdatesHub> hub)
{
_hub = hub;
}
public MyUpdateFunction()
{
//Update database
//
//
_hub.Clients.All.SendAsync("ReceiveMessage", "Database updated!!");
}
...
I'm facing a strange issue since I created a view component in my app.
All my integration tests using an HttpClient start failing with response code "Internal Server Error".
Here the test configuration :
public class BaseTest<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly bool _hasUser;
private readonly HttpClient _client;
protected BaseTest(bool hasUser = false)
{
_hasUser = hasUser;
_client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
}
protected async Task<HttpResponseMessage> GetPageByPath(string path)
{
return await _client.GetAsync(path);
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
if (_hasUser)
{
services.AddScoped<IAuthenticationService, AuthenticationServiceStub>();
services.AddSingleton<IStartupFilter, FakeUserFilter>();
services.AddMvc(options =>
{
options.Filters.Add(new AllowAnonymousFilter());
options.Filters.Add(new AuthenticatedAttribute());
});
}
});
builder.ConfigureServices(services =>
{
// Create a new service provider.
ServiceProvider serviceProvider = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
// 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 logger = scopedServices
.GetRequiredService<ILogger<BaseTest<TStartup>>>();
}
});
}
}
}
And a usage example :
public class BasicTest : BaseTest<Startup>
{
public BasicTest() : base()
{
}
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/Users/SignOut")]
[Trait("Category", "Integration")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Act
var response = await GetPageByPath(url);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
If you need the component code let me know.
I already rollback code to check when this start happening, and it's start after the commit with the new Component called in several pages.
I noticed that after I restart my ASP .NET API and send a POST request the API routes to the GET request method and then my POST request method. This only happens on the first POST request after I restart the API. Each POST request after this one routes directly to my POST method without processing the GET request method. Below is the class methods from my API.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
//public class variable
private readonly fujimiContext _context;
//constructor for ValuesController class
public ValuesController(fujimiContext context)
{
_context = context;
}
// GET api/values
// [Authorize(Policy = "RequireReadRole")]
[HttpGet("bins")]
public async Task<IActionResult> GetValues()
{
var values = await _context.FcbinCnfg.ToListAsync();
return Ok(values);
}
// POST api/values
// [Authorize(Policy = "RequireEditRole")]
[HttpPost("sitepost")]
public async Task<IActionResult> Post([FromBody] FcbinCnfg [] fcbincnfg)
{
if (fcbincnfg == null)
{
throw new ArgumentNullException(nameof(fcbincnfg));
}
string WindUser = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
string AppName = System.AppDomain.CurrentDomain.FriendlyName;
if (ModelState.IsValid)
{
int i = 0;
foreach (var obj in fcbincnfg){
_context.Update(fcbincnfg[i]);
i++;
}
await _context.SaveChangesAsync();
return StatusCode(201);
}
return BadRequest("this does not work");
}
Startup.cs file
namespace FcBin.API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
var connection = #"Server=XXXXX\XXXX;Database=XXXXX;Trusted_Connection=True;";
services.AddDbContext<fujimiContext>(options => options.UseSqlServer(connection));
services.AddCors();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// app.UseHsts();
}
app.UseCors(x => x.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod().AllowCredentials());
app.UseMvc();
}
}
}
Angular Route
import { Routes} from '#angular/router';
import { BinConfigComponent } from './BinConfig/BinConfig.component';
import { BinResolver } from './_resolver/bin.resolver';
export const appRoutes: Routes = [
{path: 'binconfig'
, component: BinConfigComponent
, resolve: {binconfig: BinResolver}, runGuardsAndResolvers: 'always'},
{path: '', redirectTo: 'binconfig', pathMatch: 'full', runGuardsAndResolvers: 'always'}
];
So the issue had nothing to do with my routes or the API. The issue was with my save() function in my Angular front end. I approached the function as a sequential problem when in actuality the browser/client approached my function from and efficiency stand point. Below is what I had that the browser would try to optimize
save() {
if (this.dirtyFlag) {
this.dbService.dbsave(this.binconfig).subscribe( () => {
}, error => {
console.log(error);
});
}
if (this.isFoo && this.valid) {
this.dbService.dbsavebin(this.newbin).subscribe( error => {
console.log(error);
});
} else if (!this.valid && this.isFoo) {
this.toastr.warning('Enter a Bin Id');
}
this.router.navigate(['/binconfig']);
}
Here I was having a route resolver reload the page which I triggered after a save. Tacking the route at the end of this save would result in the browser trying to optimize the POST / GET methods in the save() function and in the route resolver. I solved the issue by instead using the arrow function to execute the router navigation after a successful save.
save() {
if (this.dirtyFlag) {
this.dbService.dbsave(this.binconfig).subscribe( () => {
}, error => {
console.log(error);
}, () => {
this.router.navigate(['/binconfig']);
this.toastr.success('Changes Saved');
this.dirtyFlag = false;
});
}
if (this.isFoo && this.valid) {
this.dbService.dbsavebin(this.newbin).subscribe( () => {
this.router.navigate(['/binconfig']);
this.toastr.success('New Bin Added');
this.isFoo = false;
}, error => {
console.log(error);
});
} else if (!this.valid && this.isFoo) {
this.toastr.warning('Enter a Bin Id');
}
}
I am trying to add swagger+swashbuckle to my ASP.NET Core project. I can get the Swagger UI up and running but it is completely empty. I tried poking around and found a similar problem at https://github.com/domaindrivendev/Swashbuckle/issues/1058. This made me think that maybe the routing was the issue so I tried giving my controller an explicit route using [Route("testroute")] over the method but not the class. This made the endpoints I added a route to show up with no problem.
As adding an explicit route to every endpoint is non-optimal, what am I doing wrong and how do I fix it to get swagger to show all my endpoints?
My startup where swagger is integrated
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true , reloadOnChange: true);
Configuration = builder.Build();
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
});
services.AddDbContext<PromotionContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:Jasmine"]));
services.AddTransient<PromotionDbInitializer>();
services.AddTransient<IComponentHelper, ComponentHelper>();
services.AddTransient<IComponentFileHelper, ComponentFileHelper>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, PromotionDbInitializer promotionSeeder)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Promotion}/{action=Index}/{id?}");
});
//Because there is not a seed method built into the EF migrations pipeline in EFCore this seeding method will interfere with the migrations when attempting to deploy the database
//uncomment if you need to seed
//promotionSeeder.Seed().Wait();
}
}
My controller where the GetAll and Post method show up on the swagger ui page under testroute and testFormRoute, but the Get and Delete methods do not show up
public class PromotionController : Controller
{
private PromotionContext context;
public PromotionController(PromotionContext _context)
{
context = _context;
}
public IActionResult Index()
{
return View();
}
[HttpGet]
[Route("testroute")]
public IActionResult GetAll()
{
try
{
var result = context.Promotions
.Include(promotion => promotion.CombinabilityType)
.Include(promotion => promotion.ValueType)
.Include(promotion => promotion.Currency)
.Include(promotion => promotion.Components)
.ThenInclude(component => component.TargetType)
.ToList();
return Ok(result);
}
catch(Exception ex)
{
return StatusCode(500);
}
}
public IActionResult Get(string promoCode)
{
try
{
var result = context.Promotions
.Include(promotion => promotion.CombinabilityType)
.Include(promotion => promotion.ValueType)
.Include(promotion => promotion.Currency)
.Include(promotion => promotion.Components)
.ThenInclude(component => component.TargetType)
.FirstOrDefault(x => x.PromoCode == promoCode);
return Ok(result);
}
catch(Exception ex)
{
return StatusCode(500);
}
}
[HttpPost]
[Route("testFormRoute")]
public IActionResult Post([FromForm] Promotion newPromotion)
{
try
{
context.Promotions.Add(newPromotion);
context.SaveChanges();
}
catch(DbUpdateException ex)
{
return StatusCode(500);
}
return Ok();
}
[HttpDelete]
public IActionResult Delete(string promoCode)
{
try
{
var promotion = context.Promotions.FirstOrDefault(x => x.PromoCode == promoCode);
if(promotion != null)
{
context.Promotions.Remove(promotion);
context.SaveChanges();
}
}
catch(DbUpdateException ex)
{
return StatusCode(500);
}
return Ok();
}
}
Add a route attribute to your controller:
[Route("[controller]/[action]")]
public class PromotionController : Controller
{
...
And set the HttpGet attribute on your actions:
[HttpGet]
public IActionResult GetAll()
{
...
[HttpGet("{promoCode}")]
public IActionResult Get(string promoCode)
{
...
You have to be careful about how you mix and match static and dynamic routes. Check out this article for more detail about attribute based routing in asp.net core.
Try changing
public class PromotionController : Controller
to
public class PromotionController : ApiController