Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests that make sure user provided implementations of interfaces have expected ServiceLifetime #779

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

martinothamar
Copy link
Contributor

Description

Proposal for solution

Related Issue(s)

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green

Documentation

  • User documentation is updated with a separate linked PR in altinn-studio-docs. (if applicable)

@martinothamar martinothamar requested a review from a team September 17, 2024 10:48
@martinothamar
Copy link
Contributor Author

Nevermind, this doesn't actually work... Working on a new suggestion

@ivarne
Copy link
Member

ivarne commented Sep 17, 2024

I think the correct approach would be to have a shared factory for all extension point interfaces, that are registered as a Scoped service and retrieves services from a IServiceProvider (that inherits the scope). That way we can also add telemetry span around the GetRequiredService call to catch potentially slow constructors written by users.

@martinothamar martinothamar force-pushed the chore/app-implementation-factory-and-tests branch from 246ef16 to 2206625 Compare September 19, 2024 11:57
/// Marker attribute for interfaces that are meant to be implemented by apps.
/// </summary>
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
internal sealed class ImplementableByAppsAttribute : Attribute { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What benefits will marking interfaces in this way give? Can it make them show up as suggestions in app developers editors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR just as a mechanism to categorize. I could make a list of interfaces, but then it is hard to know that the list should be updated when an interface is added. There is future potential as well. Main point is having some mechanism to categorize the interfaces (since it is public). Could also be the inverse - public interfaces that are not meant to be public are attributed with Pubternal or something

{
internal sealed record DIScopeHolder(IServiceProvider? ScopedServiceProvider);

private readonly DIScopeHolder _diScopeHolder;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this concept and the additional indirection a little hard to understand.

The straight forward solution would be to just inject a scoped ServiceProvider and use reflection to access the IsRootScope to verify when running in debug mode (as we do in tests, but not when apps are running)

#if DEBUG
        var serviceProviderType = serviceProvider.GetType();
        if (
            serviceProviderType.FullName
            != "Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope"
        )
        {
            throw new InvalidOperationException(
                "InterfaceFactory expects ServiceProviderEngineScope to run debug scope validation"
            );
        }
        var rootProperty = serviceProviderType.GetProperty(
            "IsRootScope",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance
        );
        if (rootProperty == null)
        {
            throw new InvalidOperationException(
                "InterfaceFactory expects ServiceProviderEngineScope to have IsRootScope property"
            );
        }
        var rootPropertyValue = rootProperty.GetValue(serviceProvider);
        switch (rootPropertyValue)
        {
            case true:
                throw new InvalidOperationException("InterfaceFactory should not be used in root scope");
            case false:
                break;
            default:
                throw new InvalidOperationException("InterfaceFactory expects IsRootScope to be a boolean");
        }
#endif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm so my only intention with this thing was to enable HTTP-less test fixtures, and that's it. Might have given it a poor name 😄 So ideally what we go with only changes behavior for those specific cases (simple tests using only a DI container). Since for all other cases (app runtime, integration tests) we want the IHttpContextAccessor request scope, I'm not sure if the code you posted is justified. To me it seems complicating rather than simplifying

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having a constructor (IServiceProivder sp) is simpler (especially for tests) than having to wrap it in an internal DIScopeHolder. The code I posted is just part of an internal verification, and not the how the rest of the code base uses the interface factory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests don't need to care about that though. There are the 2 DI methods for registering the factory as a service, so in that sense I would say this code is internal to the factory. So the two "modes" of execution for the current code is

  1. AddAppImplementationFactory -> IHttpContextAccessor.HttpContext.RequestServices (for real/app and integration test use)
  2. AddTestAppImplementationFactory -> Root container (for simple tests where we don't want to spin up ASP.NET Core test host and whatnot)

I added a commit where I get rid of the so called "scope holder" and just use a bool in the constructor instead. So in the first mode we will get an exception if there is no request scope, and in the test mode I don't think we really care

Copy link
Contributor

@HauklandJ HauklandJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the approach, some comments

@danielskovli
Copy link
Contributor

@martinothamar I don't a useful opinion or contribution to this discussion, unfortunately. It would be great to get a summary of the solution at some point though, after the details have been worked out.

Comment on lines +33 to +39
if (testMode)
_getServiceProvider = () => sp;
else
// Right now we are just using the HttpContext to get the current scope,
// in the future we might not be always running in a web context,
// at that point we need to replace this
_getServiceProvider = () => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.RequestServices;

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
24.3% Coverage on New Code (required ≥ 65%)
33.33% Condition Coverage on New Code (required ≥ 65%)

See analysis details on SonarCloud

@bjorntore
Copy link
Contributor

Really like the attribute plus DiagnosticAnalyzer approach. No objections.

@martinothamar martinothamar force-pushed the chore/app-implementation-factory-and-tests branch 2 times, most recently from b539175 to ef2b1b4 Compare October 30, 2024 14:29
@@ -77,7 +78,8 @@

// runs prefill from repo configuration if config exists
await _prefillService.PrefillDataModel(instance.InstanceOwner.PartyId, dataType.Id, data, prefill);
await _instantiationProcessor.DataCreation(instance, data, prefill);
var instantiationProcessor = _appImplementationFactory.GetRequired<IInstantiationProcessor>();
await instantiationProcessor.DataCreation(instance, data, prefill);

Check failure

Code scanning / CodeQL

Bad dynamic call Error

The
target
of this dynamic method invocation can obtain (from
here
) type
NullInstantiationProcessor
, which does not have a method 'DataCreation' with the appropriate signature.

// Act
ActionResult actual = await SUT.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());
var controller = fixture.ServiceProvider.GetRequiredService<InstancesController>();
ActionResult actual = await controller.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());

Check warning

Code scanning / CodeQL

Call to obsolete method Warning test

Call to obsolete method
CopyInstance
.
.Setup(a => a.GetApplicationMetadata())
.ReturnsAsync(CreateApplicationMetadata(Org, AppName, false));

// Act
ActionResult actual = await SUT.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());
var controller = fixture.ServiceProvider.GetRequiredService<InstancesController>();
ActionResult actual = await controller.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());

Check warning

Code scanning / CodeQL

Call to obsolete method Warning test

Call to obsolete method
CopyInstance
.

// Act
ActionResult actual = await SUT.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());
var controller = fixture.ServiceProvider.GetRequiredService<InstancesController>();
ActionResult actual = await controller.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());

Check warning

Code scanning / CodeQL

Call to obsolete method Warning test

Call to obsolete method
CopyInstance
.
.ReturnsAsync(CreateXacmlResponse("Deny"));

// Act
ActionResult actual = await SUT.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());
var controller = fixture.ServiceProvider.GetRequiredService<InstancesController>();
ActionResult actual = await controller.CopyInstance("ttd", "copy-instance", 343234, Guid.NewGuid());

Check warning

Code scanning / CodeQL

Call to obsolete method Warning test

Call to obsolete method
CopyInstance
.
@@ -416,20 +411,26 @@
.ReturnsAsync(new DataElement());

// Act
ActionResult actual = await SUT.CopyInstance(Org, AppName, InstanceOwnerPartyId, instanceGuid);
var controller = fixture.ServiceProvider.GetRequiredService<InstancesController>();
ActionResult actual = await controller.CopyInstance(Org, AppName, InstanceOwnerPartyId, instanceGuid);

Check warning

Code scanning / CodeQL

Call to obsolete method Warning test

Call to obsolete method
CopyInstance
.
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(httpResponseMessage);
HttpClient httpClient = new HttpClient(handlerMock.Object);

Check warning

Code scanning / CodeQL

Missing Dispose call on local IDisposable Warning test

Disposable 'HttpClient' is created but not disposed.
"action",
userActionAuthorizerMock.Object
);
IUserActionAuthorizerProvider userActionAuthorizerProvider = fixture.UserActionAuthorizerProvider;

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning test

This assignment to
userActionAuthorizerProvider
is useless, since its value is never read.
// provider.Should().ContainEquivalentOf(new UserActionAuthorizerProvider(taskId2, action, authorizer.First()));
provider
.Should()
.ContainSingle(p => p.TaskId == taskId && p.Action == action && p.Authorizer == authorizer.First());

Check warning

Code scanning / CodeQL

Reference equality test on System.Object Warning test

Reference equality for System.Object comparisons (
this
argument has type IUserActionAuthorizer).
.ContainSingle(p => p.TaskId == taskId && p.Action == action && p.Authorizer == authorizer.First());
provider
.Should()
.ContainSingle(p => p.TaskId == taskId2 && p.Action == action && p.Authorizer == authorizer.First());

Check warning

Code scanning / CodeQL

Reference equality test on System.Object Warning test

Reference equality for System.Object comparisons (
this
argument has type IUserActionAuthorizer).
@martinothamar martinothamar force-pushed the chore/app-implementation-factory-and-tests branch from d0226a6 to ca9d7e7 Compare November 27, 2024 13:11
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
62.1% Coverage on New Code (required ≥ 65%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants