Skip to content

Commit f17c14e

Browse files
author
Espen Gulbrandsen
committed
Move logs to its own controller
1 parent 5f8b141 commit f17c14e

File tree

8 files changed

+243
-188
lines changed

8 files changed

+243
-188
lines changed

src/Events.Functions/Models/LogEntryDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Altinn.Platform.Events.Functions.Models
66
/// <summary>
77
/// Data transfer object for posting a log event after receiving a webhook response.
88
/// </summary>
9-
public class LogEntryDto
9+
public record LogEntryDto
1010
{
1111
/// <summary>
1212
/// The cloud event id associated with the logged event
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
using Altinn.Platform.Events.Models;
5+
using Altinn.Platform.Events.Services.Interfaces;
6+
7+
using Microsoft.AspNetCore.Authorization;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Mvc;
10+
using Swashbuckle.AspNetCore.Annotations;
11+
12+
namespace Altinn.Platform.Events.Controllers
13+
{
14+
/// <summary>
15+
/// Controller for logging event operations to persistence
16+
/// </summary>
17+
/// <param name="traceLogService">Service for logging event operations to persistence</param>
18+
[Route("events/api/v1/storage/events/logs")]
19+
[ApiController]
20+
[SwaggerTag("Private API")]
21+
public class LogsController(ITraceLogService traceLogService) : ControllerBase
22+
{
23+
private readonly ITraceLogService _traceLogService = traceLogService;
24+
25+
/// <summary>
26+
/// Create a new trace log for cloud event with a status code.
27+
/// </summary>
28+
/// <param name="logEntry">The event wrapper associated with the event for logging <see cref="CloudEventEnvelope"/></param>
29+
/// <returns></returns>
30+
[Authorize(Policy = "PlatformAccess")]
31+
[HttpPost]
32+
[Consumes("application/json")]
33+
[SwaggerResponse(201, Type = typeof(Guid))]
34+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
35+
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
36+
public async Task<IActionResult> Logs([FromBody] LogEntryDto logEntry)
37+
{
38+
var result = await _traceLogService.CreateWebhookResponseEntry(logEntry);
39+
40+
if (string.IsNullOrEmpty(result))
41+
{
42+
return BadRequest();
43+
}
44+
45+
return Created();
46+
}
47+
}
48+
}

src/Events/Controllers/StorageController.cs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22
using System.Diagnostics.CodeAnalysis;
33
using System.Threading.Tasks;
44

5-
using Altinn.Platform.Events.Models;
65
using Altinn.Platform.Events.Services.Interfaces;
76

87
using CloudNative.CloudEvents;
9-
108
using Microsoft.ApplicationInsights.DataContracts;
119
using Microsoft.AspNetCore.Authorization;
1210
using Microsoft.AspNetCore.Http;
1311
using Microsoft.AspNetCore.Mvc;
1412
using Microsoft.Extensions.Logging;
15-
1613
using Swashbuckle.AspNetCore.Annotations;
1714

1815
namespace Altinn.Platform.Events.Controllers
@@ -26,20 +23,17 @@ namespace Altinn.Platform.Events.Controllers
2623
public class StorageController : ControllerBase
2724
{
2825
private readonly IEventsService _eventsService;
29-
private readonly ITraceLogService _traceLogService;
3026
private readonly ILogger _logger;
3127

3228
/// <summary>
3329
/// Initializes a new instance of the <see cref="StorageController"/> class
3430
/// </summary>
3531
public StorageController(
3632
IEventsService eventsService,
37-
ITraceLogService traceLogService,
3833
ILogger<StorageController> logger)
3934
{
4035
_logger = logger;
4136
_eventsService = eventsService;
42-
_traceLogService = traceLogService;
4337
}
4438

4539
/// <summary>
@@ -69,29 +63,6 @@ public async Task<ActionResult<string>> Post([FromBody] CloudEvent cloudEvent)
6963
}
7064
}
7165

72-
/// <summary>
73-
/// Create a new trace log for cloud event with a status code.
74-
/// </summary>
75-
/// <param name="logEntry">The event wrapper associated with the event for logging <see cref="CloudEventEnvelope"/></param>
76-
/// <returns></returns>
77-
[Authorize(Policy = "PlatformAccess")]
78-
[HttpPost("logs")]
79-
[Consumes("application/json")]
80-
[SwaggerResponse(201, Type = typeof(Guid))]
81-
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
82-
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
83-
public async Task<IActionResult> Logs([FromBody] LogEntryDto logEntry)
84-
{
85-
var result = await _traceLogService.CreateWebhookResponseEntry(logEntry);
86-
87-
if (string.IsNullOrEmpty(result))
88-
{
89-
return BadRequest();
90-
}
91-
92-
return Created();
93-
}
94-
9566
[ExcludeFromCodeCoverage]
9667
private void AddIdTelemetry(string id)
9768
{

src/Events/Models/LogEntryDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Altinn.Platform.Events.Models
99
/// <summary>
1010
/// Data transfer object for posting a log event after receiving a webhook response.
1111
/// </summary>
12-
public class LogEntryDto
12+
public record LogEntryDto
1313
{
1414
/// <summary>
1515
/// The cloud event id associated with the logged event <see cref="CloudEvent"/>"/>

src/Events/Services/TraceLogService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using System;
44
using System.Linq;
55
using System.Threading.Tasks;
6-
using System.Web;
6+
77
using Altinn.Platform.Events.Extensions;
88
using Altinn.Platform.Events.Models;
99
using Altinn.Platform.Events.Repository;
@@ -46,7 +46,7 @@ public async Task<string> CreateRegisteredEntry(CloudEvent cloudEvent)
4646
}
4747
catch (Exception exception)
4848
{
49-
_logger.LogError(exception, "Error creating trace log entry for registered event: {Message}", exception.Message);
49+
_logger.LogError(exception, "Error creating trace log entry for registered event: {Message} {CloudEventId}", exception.Message, cloudEvent.Id);
5050

5151
// don't throw exception, we don't want to stop the event processing
5252
return string.Empty;
@@ -77,7 +77,7 @@ public async Task<string> CreateLogEntryWithSubscriptionDetails(CloudEvent cloud
7777
}
7878
catch (Exception exception)
7979
{
80-
_logger.LogError(exception, "Error creating trace log entry with subscription details: {Message}", exception.Message);
80+
_logger.LogError(exception, "Error creating trace log entry with subscription details: {Message} {CloudEventId} {SubscriptionId} {Consumer}", exception.Message, cloudEvent.Id, subscription.Id, subscription.Consumer);
8181

8282
// don't throw exception, we don't want to stop the event processing
8383
return string.Empty;

test/Altinn.Platform.Events.Tests/Models/LogEntryData.cs

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Text;
5+
using System.Text.Json;
6+
using System.Threading.Tasks;
7+
8+
using Altinn.Common.AccessToken.Services;
9+
using Altinn.Common.PEP.Interfaces;
10+
using Altinn.Platform.Events.Controllers;
11+
using Altinn.Platform.Events.Models;
12+
using Altinn.Platform.Events.Services.Interfaces;
13+
using Altinn.Platform.Events.Tests.Mocks;
14+
using Altinn.Platform.Events.Tests.Mocks.Authentication;
15+
using Altinn.Platform.Events.Tests.Utils;
16+
using Altinn.Platform.Events.UnitTest.Mocks;
17+
using AltinnCore.Authentication.JwtCookie;
18+
19+
using Microsoft.AspNetCore.Mvc.Testing;
20+
using Microsoft.AspNetCore.TestHost;
21+
using Microsoft.Extensions.Configuration;
22+
using Microsoft.Extensions.DependencyInjection;
23+
using Microsoft.Extensions.Options;
24+
using Moq;
25+
using Xunit;
26+
27+
namespace Altinn.Platform.Events.Tests.TestingControllers;
28+
29+
/// <summary>
30+
/// Reperesents a collection of integration tests.
31+
/// </summary>
32+
public partial class IntegrationTests
33+
{
34+
/// <summary>
35+
/// Test class for LogsController
36+
/// </summary>
37+
public class LogsControllerTests : IClassFixture<WebApplicationFactory<LogsController>>
38+
{
39+
private const string _basePath = "/events/api/v1";
40+
41+
private readonly WebApplicationFactory<LogsController> _factory;
42+
43+
private readonly Mock<ITraceLogService> _traceLogServiceMock = new();
44+
45+
public LogsControllerTests(WebApplicationFactory<LogsController> factory)
46+
{
47+
_factory = factory;
48+
}
49+
50+
/// <summary>
51+
/// Scenario:
52+
/// Post a request that results in an exception being thrown by the trace log service instance.
53+
/// Expected result:
54+
/// Returns internal server error 500.
55+
/// Success criteria:
56+
/// The response has correct status code.
57+
/// </summary>
58+
[Fact]
59+
public async Task Logs_WhenExceptionIsThrownByTheService_IsCaughtAnd500IsReturned()
60+
{
61+
// Arrange
62+
string requestUri = $"{_basePath}/storage/events/logs";
63+
string responseId = Guid.NewGuid().ToString();
64+
65+
Mock<IEventsService> eventsService = new();
66+
Mock<ITraceLogService> traceLogService = new();
67+
68+
traceLogService.Setup(x => x.CreateWebhookResponseEntry(It.IsAny<LogEntryDto>())).Throws(new Exception());
69+
70+
HttpClient client = GetTestClient(eventsService.Object, traceLogService.Object);
71+
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)
72+
{
73+
Content = new StringContent(JsonSerializer.Serialize(new LogEntryDto()), Encoding.UTF8, "application/cloudevents+json")
74+
};
75+
76+
httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "endring-av-navn-v2"));
77+
78+
// Act
79+
HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
80+
81+
// Assert
82+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
83+
}
84+
85+
/// <summary>
86+
/// Scenario:
87+
/// A possibly invalid log entry is posted to the logs endpoint.
88+
/// Expected result:
89+
/// 400 Bad request is returned
90+
/// Success criteria:
91+
/// Empty string returned from the service results in a bad request response
92+
/// </summary>
93+
/// <returns></returns>
94+
[Fact]
95+
public async Task Logs_WhenEmptyStringIsReturnedFromTheService_ReturnBadRequest()
96+
{
97+
// Arrange
98+
string requestUri = $"{_basePath}/storage/events/logs";
99+
string responseId = string.Empty;
100+
Mock<IEventsService> eventsService = new();
101+
Mock<ITraceLogService> traceLogService = new();
102+
traceLogService.Setup(x => x.CreateWebhookResponseEntry(It.IsAny<LogEntryDto>())).ReturnsAsync(string.Empty);
103+
HttpClient client = GetTestClient(eventsService.Object, traceLogService.Object);
104+
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)
105+
{
106+
Content = new StringContent(JsonSerializer.Serialize(new LogEntryDto()), Encoding.UTF8, "application/cloudevents+json")
107+
};
108+
httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "endring-av-navn-v2"));
109+
110+
// Act
111+
HttpResponseMessage response = await client.SendAsync(httpRequestMessage);
112+
113+
// Assert
114+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
115+
}
116+
117+
/// <summary>
118+
/// Scenario:
119+
/// Post a valid logs request with a logEntryDto.
120+
/// Expected result:
121+
/// Returns HttpStatus Ok.
122+
/// Success criteria:
123+
/// The response has correct status code.
124+
/// </summary>
125+
[Fact]
126+
public async Task Logs_ValidLogEntryDto_ReturnsStatusCreated()
127+
{
128+
// Arrange
129+
string requestUri = $"{_basePath}/storage/events/logs";
130+
string responseId = Guid.NewGuid().ToString();
131+
132+
Mock<IEventsService> eventsService = new Mock<IEventsService>();
133+
Mock<ITraceLogService> traceLogService = new Mock<ITraceLogService>();
134+
135+
traceLogService.Setup(x => x.CreateWebhookResponseEntry(It.IsAny<LogEntryDto>())).ReturnsAsync(responseId);
136+
137+
var client = GetTestClient(eventsService.Object, traceLogService.Object);
138+
139+
string logEntryDto = JsonSerializer.Serialize(new LogEntryDto());
140+
141+
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri)
142+
{
143+
Content = new StringContent(logEntryDto, Encoding.UTF8, "application/cloudevents+json")
144+
};
145+
146+
httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "endring-av-navn-v2"));
147+
148+
// Act
149+
var response = await client.SendAsync(httpRequestMessage);
150+
151+
// Assert
152+
Assert.True(response.IsSuccessStatusCode);
153+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
154+
}
155+
156+
private HttpClient GetTestClient(IEventsService eventsService, ITraceLogService traceLogService = null)
157+
{
158+
HttpClient client = _factory.WithWebHostBuilder(builder =>
159+
{
160+
builder.ConfigureAppConfiguration((hostingContext, config) =>
161+
{
162+
config.AddConfiguration(new ConfigurationBuilder().AddJsonFile("appsettings.unittest.json").Build());
163+
});
164+
165+
builder.ConfigureTestServices(services =>
166+
{
167+
services.AddSingleton(eventsService);
168+
169+
if (traceLogService != null)
170+
{
171+
services.AddSingleton(traceLogService);
172+
}
173+
else
174+
{
175+
services.AddSingleton(_traceLogServiceMock.Object);
176+
}
177+
178+
// Set up mock authentication so that not well known endpoint is used
179+
services.AddSingleton<IPostConfigureOptions<JwtCookieOptions>, JwtCookiePostConfigureOptionsStub>();
180+
services.AddSingleton<IPublicSigningKeyProvider, PublicSigningKeyProviderMock>();
181+
services.AddSingleton<IPDP, PepWithPDPAuthorizationMockSI>();
182+
});
183+
}).CreateClient();
184+
185+
return client;
186+
}
187+
}
188+
}

0 commit comments

Comments
 (0)