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

[dotnet] [bidi] Implement Permissions extension #15425

Draft
wants to merge 18 commits into
base: trunk
Choose a base branch
from

Conversation

RenderMichael
Copy link
Contributor

@RenderMichael RenderMichael commented Mar 15, 2025

User description

Description

Maintain JSON converters with their respective types

Motivation and Context

With the current BiDi extensions work, the JSON serialization options/context will be moving around.

Referencing the stateless converters for WebDriver types directly from those types, will lighten the load on the JSON serialization work.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Enhancement, Tests


Description

  • Refactored BiDi connection and module architecture for better modularity.

  • Introduced a new Permissions extension for BiDi.

  • Added JSON serialization improvements and custom converters.

  • Implemented comprehensive unit tests for Permissions functionality.


Changes walkthrough 📝

Relevant files
Enhancement
17 files
BiDi.cs
Refactor BiDi class to use BiDiConnection                               
+57/-19 
BiDiConnection.cs
Introduce BiDiConnection for managing connections               
+55/-64 
BiDiConnectionJsonSerializerContext.cs
Add BiDiConnectionJsonSerializerContext for JSON serialization
+72/-0   
BiDiJsonSerializerContext.cs
Remove redundant JSON serialization attributes                     
+0/-17   
BiDiDateTimeOffsetConverter.cs
Rename and update DateTimeOffsetConverter                               
+2/-2     
Message.cs
Add JSON converter for Message class                                         
+4/-0     
EventArgs.cs
Update EventArgs to use BiDiConnection                                     
+4/-3     
PermissionsJsonSerializerContext .cs
Add JSON serializer context for Permissions                           
+26/-0   
PermissionsModule.cs
Implement PermissionsModule for managing permissions         
+42/-0   
SetPermissionCommand.cs
Add SetPermissionCommand for Permissions extension             
+39/-0   
BrowserModule.cs
Update BrowserModule to use BiDiConnection                             
+1/-1     
BrowsingContextInfo.cs
Update BrowsingContextInfo to use BiDiConnection                 
+2/-1     
InputModule.cs
Update InputModule to use BiDiConnection                                 
+1/-1     
LogModule.cs
Update LogModule to use BiDiConnection                                     
+1/-1     
ScriptModule.cs
Update ScriptModule to use BiDiConnection                               
+1/-1     
SessionModule.cs
Update SessionModule to use BiDiConnection                             
+1/-1     
WebDriver.Extensions.cs
Add BiDiConnection support in WebDriver extensions             
+22/-6   
Tests
2 files
BiDiFixture.cs
Refactor BiDiFixture to support BiDiConnection                     
+6/-1     
PermissionsTests.cs
Add unit tests for Permissions extension                                 
+64/-0   
Additional files
34 files
ClientWindow.cs +4/-0     
GetClientWindowsCommand.cs +3/-0     
GetUserContextsCommand.cs +3/-0     
BrowsingContextModule.cs +1/-1     
LocateNodesCommand.cs +3/-0     
Navigation.cs +4/-0     
NavigationInfo.cs +3/-1     
PrintCommand.cs +3/-0     
UserPromptClosedEventArgs.cs +2/-1     
UserPromptOpenedEventArgs.cs +2/-1     
Origin.cs +4/-0     
SourceActions.cs +2/-0     
LogEntry.cs +8/-3     
Module.cs +3/-2     
AuthRequiredEventArgs.cs +3/-1     
BaseParametersEventArgs.cs +2/-1     
BeforeRequestSentEventArgs.cs +3/-1     
FetchErrorEventArgs.cs +3/-1     
NetworkModule.cs +1/-1     
ResponseCompletedEventArgs.cs +3/-1     
ResponseStartedEventArgs.cs +3/-1     
Channel.cs +4/-0     
EvaluateCommand.cs +4/-0     
GetRealmsCommand.cs +3/-0     
MessageEventArgs.cs +4/-1     
RealmDestroyedEventArgs.cs +4/-1     
RealmInfo.cs +14/-9   
RealmType.cs +4/-0     
RemoteValue.cs +3/-0     
Subscription.cs +4/-0     
GetCookiesCommand.cs +2/-0     
StorageModule.cs +1/-1     
Subscription.cs +2/-2     
index.rst [link]   

Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • Copy link
    Contributor

    qodo-merge-pro bot commented Mar 15, 2025

    PR Reviewer Guide 🔍

    (Review updated until commit c604f3c)

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Connection Management

    The refactoring of BiDi connection architecture introduces a new ownership model with _ownsConnection flag. Verify that connection disposal logic is correctly implemented to prevent resource leaks when connections are shared across multiple components.

    private BiDi(BiDiConnection connection) : this()
    {
        _ownsConnection = false;
        BiDiConnection = connection;
        AddBiDiModuleJsonInfo(connection);
    }
    
    private BiDi(string url) : this()
    {
        _ownsConnection = true;
        BiDiConnection = new BiDiConnection(new Uri(url));
        AddBiDiModuleJsonInfo(BiDiConnection);
    }
    JSON Serialization

    The new serialization approach with dynamic registration of converters and contexts could lead to thread-safety issues if AddSerializerContextAndConverters is called after connection is established. Verify proper validation and error handling.

    public void AddSerializerContextAndConverters(JsonSerializerContext context, IList<JsonConverter>? converters = null)
    {
        if (_jsonSerializerContext.IsReadOnly)
        {
            throw new InvalidOperationException("Cannot add JSON serializer context after ConnectAsync has been called");
        }
    
        _jsonSerializerContext.TypeInfoResolverChain.Add(context);
        foreach (JsonConverter converter in converters ?? [])
        {
            _jsonSerializerContext.Converters.Add(converter);
        }
    }
    Error Handling

    The new Permissions extension lacks error handling for cases where the browser might not support the requested permission or when invalid permission values are provided.

    public async Task SetPermissionAsync(string permissionName, PermissionState state, string origin, UserContext? userContext, SetPermissionOptions? options = null)
    {
        var @params = new SetPermissionCommandParameters(new PermissionDescriptor(permissionName), state, origin, userContext);
    
        await Broker.ExecuteCommandAsync(new SetPermissionCommand(@params), options).ConfigureAwait(false);
    }

    Copy link
    Contributor

    qodo-merge-pro bot commented Mar 15, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix filename space character

    The filename has an extra space character before the extension, which will cause
    issues with file resolution. The space should be removed from the filename.

    dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsJsonSerializerContext .cs [1]

    -// <copyright file="PermissionsJsonSerializerContext .cs" company="Selenium Committers">
    +// <copyright file="PermissionsJsonSerializerContext.cs" company="Selenium Committers">
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    __

    Why: This suggestion identifies a critical issue with the filename having an extra space character before the extension, which would cause file resolution problems and build errors. Fixing this is important for proper compilation.

    Medium
    Make test more resilient

    The test is making an assumption about the initial permission state being
    "prompt", but this might not be consistent across all browser environments. The
    test should be more resilient by not relying on a specific initial state, but
    instead verifying that the permission state changes after calling
    SetPermissionAsync.

    dotnet/test/common/BiDi/Permissions/PermissionsTests.cs [49-61]

     var before = await window.Script.CallFunctionAsync("""
     async () => (await navigator.permissions.query({ name: "geolocation" })).state
     """, awaitPromise: true, new() { UserActivation = true, });
     
    -Assert.That(before.Result, Is.EqualTo(new StringRemoteValue("prompt")));
    +// Store initial state, then set to a different state
    +string initialState = before.Result.ToString();
    +PermissionState newState = initialState == "denied" ? PermissionState.Granted : PermissionState.Denied;
     
    -await _permissions.SetPermissionAsync("geolocation", PermissionState.Denied, newPage, userContext.UserContext);
    +await _permissions.SetPermissionAsync("geolocation", newState, newPage, userContext.UserContext);
     
     var after = await window.Script.CallFunctionAsync("""
     async () => (await navigator.permissions.query({ name: "geolocation" })).state
     """, awaitPromise: true, new() { UserActivation = true });
     
    +Assert.That(after.Result.ToString(), Is.Not.EqualTo(initialState));
    +

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion improves test reliability by removing the assumption about the initial permission state being "prompt". Instead, it adapts the test to work with any initial state, making it more robust across different browser environments.

    Medium
    Add parameter validation

    The method doesn't validate the permissionName parameter, which could lead to
    runtime errors if null or empty values are passed. Add parameter validation to
    ensure the permissionName is not null or empty.

    dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsModule.cs [36-41]

     public async Task SetPermissionAsync(string permissionName, PermissionState state, string origin, UserContext? userContext, SetPermissionOptions? options = null)
     {
    +    if (string.IsNullOrEmpty(permissionName))
    +    {
    +        throw new ArgumentException("Permission name cannot be null or empty", nameof(permissionName));
    +    }
    +    
    +    if (string.IsNullOrEmpty(origin))
    +    {
    +        throw new ArgumentException("Origin cannot be null or empty", nameof(origin));
    +    }
    +    
         var @params = new SetPermissionCommandParameters(new PermissionDescriptor(permissionName), state, origin, userContext);
     
         await Broker.ExecuteCommandAsync(new SetPermissionCommand(@params), options).ConfigureAwait(false);
     }
    • Apply this suggestion
    Suggestion importance[1-10]: 6

    __

    Why: The suggestion adds important input validation to prevent potential runtime errors when null or empty values are passed. This defensive programming practice improves code robustness and provides clearer error messages.

    Low
    • Update

    @RenderMichael
    Copy link
    Contributor Author

    Reason for this PR: we are moving the JSON serialization context/options, it would be easier if there were less types/types managed themselves. Especially if we are not using BiDiSerializationContext as a monolith.

    @nvborisenko
    Copy link
    Member

    I disagree.

    • Maintain all converters in one place is better than spreading all through code base
    • Currently we have stateless/stateful converters, and it is easy to see them in one place, even easy to change them
    • Throwing NotSupportedException instead of NotImplementedException is false, because of it is supported, but not yet implemented
    • Motivation is not yet clear, probably I miss something

    @nvborisenko
    Copy link
    Member

    Asked big brain to see what I may miss. Seems he agrees with me:

    In .NET using System.Text.Json, when managing numerous DTOs and custom converters, the choice between using JsonConverter attributes on types versus global registration in JsonSerializerOptions depends on maintainability, scalability, and team practices. Here's a structured comparison and recommendation:

    Using [JsonConverter] Attributes

    Pros:

    • Explicit and Local: The converter is directly declared on the DTO, making it obvious to developers inspecting the class.
    • Self-Contained: Useful for DTOs with unique serialization logic that isn’t reused elsewhere.
    • Simpler Testing: Serialization in tests automatically picks up the attribute, reducing setup code.

    Cons:

    • Decentralized Configuration: With many DTOs, managing converters requires modifying each class, leading to repetitive code.
    • Tight Coupling: Changing a converter requires editing every DTO using it, which is error-prone in large codebases.
    • No Global Overrides: Hard to replace converters centrally (e.g., for a different serialization strategy in a specific context).

    Global Registration via JsonSerializerOptions

    Pros:

    • Centralized Management: All converters are registered in one place (e.g., Startup.cs or a JSON config class), simplifying updates and oversight.
    • Flexibility: Easily swap converters for testing, different environments, or third-party integrations without altering DTOs.
    • Reusability: A single converter can be applied to multiple DTOs without duplication.
    • Framework Alignment: Fits well with ASP.NET Core’s pattern of configuring serialization options globally.

    Cons:

    • Implicit Behavior: Developers must know to check JsonSerializerOptions to understand why a DTO serializes a certain way.
    • Setup Overhead: Requires diligent registration of new converters when adding DTOs (mitigated by good documentation/processes).

    Recommendation: Prefer Global Registration
    For large-scale applications with many DTOs and converters, centralizing converters in JsonSerializerOptions is better for long-term maintenance. Benefits include:

    • Easier Refactoring: Update or replace converters in one location.
    • Consistency: Enforce uniform serialization policies across the project.
    • Reduced Boilerplate: Avoid cluttering DTOs with repetitive attributes.

    By prioritizing global registration, you streamline maintenance and reduce friction in evolving serialization logic, especially in complex applications.

    @RenderMichael
    Copy link
    Contributor Author

    Motivation is not yet clear, probably I miss something

    Motivation is, we will soon be passing in custom JsonSerializerOptions from extensions.

    The extensions use BiDi core types. For example, Permissions uses UserContext and others.

    If we keep the converters on attributes, then other options will also use those converters.

    @nvborisenko
    Copy link
    Member

    nvborisenko commented Mar 15, 2025

    Cross-referencing types between modules, accepted. Let's keep it in mind, implement Permissions module (in draft state), and only then we will have wider picture to make a decision.

    @RenderMichael
    Copy link
    Contributor Author

    Throwing NotSupportedException instead of NotImplementedException is false, because of it is supported, but not yet implemented

    Fixing this now

    @RenderMichael
    Copy link
    Contributor Author

    Cross-referencing types between modules, accepted. Let's keep it in mind, implement Permissions module (in draft state), and only then we will have wider picture to make a decision.

    I have a draft for that, #15421. The PermissionsModule is fully functional, But the PermissonsBiDi design needs work.

    Copy link
    Member

    @nvborisenko nvborisenko left a comment

    Choose a reason for hiding this comment

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

    Gathering info about expected plans, and then we will think together again for good implementation.

    @RenderMichael RenderMichael marked this pull request as draft March 22, 2025 04:31
    Comment on lines 12 to 32
    private PermissionsModule _permissions;
    private Communication.BiDiConnection _connection;

    protected override async Task<BiDi> CreateBiDi(IWebDriver driver)
    {
    _connection = await driver.AsBiDiConnectionAsync();
    _permissions = await PermissionsModule.AttachAsync(_connection);
    var bidi = await BiDi.AttachAsync(_connection);
    await _connection.ConnectAsync(default);
    return bidi;
    }

    [TearDown]
    public async Task TearDown()
    {
    if (_connection is not null)
    {
    await _connection.DisposeAsync();
    }
    }

    Copy link
    Contributor Author

    @RenderMichael RenderMichael Mar 22, 2025

    Choose a reason for hiding this comment

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

    Here is an example of open-ended usage.

    Normal BiDi:

    1. var bidi = driver.AsBiDiAsync()
    2. Use BiDi
    3. await bidi.DisposeAsync()

    Add extensions:

    1. var connection = driverAsBiDiConnectionAsync();
    2. create any BiDi modules you want (using AttachAsync pattern)
    3. await connection.ConnectAsync()
    4. Use BiDi
    5. await connection.DisposeAsync()

    Note: it is very easy to encapsulate this:

    *Simple "BiDi + permissions" example*
    public sealed class BiDiAndPermissions : IAsyncDisposable
    {
        private readonly BiDiConnection _connection;
    
        private BiDiAndPermissions(BiDiConnection connection, PermissionsModule permissions, BiDi bidi)
        {
            _connection = connection;
            Permissions = permissions;
            BiDi = bidi;
        }
    
        public PermissionsModule Permissions { get; }
        public BiDi BiDi { get; }
    
        public static async Task<BiDiAndPermissions> ConnectAsync(IWebDriver driver)
        {
            var connection = await driver.AsBiDiConnectionAsync();
            var permissions = await PermissionsModule.AttachAsync(connection);
            var bidi = await BiDi.AttachAsync(connection);
            await connection.ConnectAsync(default);
    
            return new BiDiAndPermissions(connection, permissions, bidi);
        }
    
        public async ValueTask DisposeAsync()
        {
            await _connection.DisposeAsync();
            await BiDi.DisposeAsync();
        }
    }

    @RenderMichael RenderMichael changed the title [dotnet] [bidi] Maintain JSON converters with their respective types [dotnet] [bidi] Implement Permissions extension Mar 22, 2025
    @RenderMichael RenderMichael marked this pull request as ready for review March 22, 2025 05:37
    Copy link
    Contributor

    qodo-merge-pro bot commented Mar 22, 2025

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Fix filename space character

    The filename has an extra space character before the extension, which will cause
    issues with file resolution. The space should be removed from the filename.

    dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsJsonSerializerContext .cs [1]

    -// <copyright file="PermissionsJsonSerializerContext .cs" company="Selenium Committers">
    +// <copyright file="PermissionsJsonSerializerContext.cs" company="Selenium Committers">
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    __

    Why: This suggestion identifies a critical issue with the filename having an extra space character before the extension, which would cause file resolution problems and build errors. Fixing this is important for proper compilation.

    Medium
    Make test more resilient

    The test is making an assumption about the initial permission state being
    "prompt", but this might not be consistent across all browser environments. The
    test should be more resilient by not relying on a specific initial state, but
    instead verifying that the permission state changes after calling
    SetPermissionAsync.

    dotnet/test/common/BiDi/Permissions/PermissionsTests.cs [49-61]

     var before = await window.Script.CallFunctionAsync("""
     async () => (await navigator.permissions.query({ name: "geolocation" })).state
     """, awaitPromise: true, new() { UserActivation = true, });
     
    -Assert.That(before.Result, Is.EqualTo(new StringRemoteValue("prompt")));
    +// Store initial state, then set to a different state
    +string initialState = before.Result.ToString();
    +PermissionState newState = initialState == "denied" ? PermissionState.Granted : PermissionState.Denied;
     
    -await _permissions.SetPermissionAsync("geolocation", PermissionState.Denied, newPage, userContext.UserContext);
    +await _permissions.SetPermissionAsync("geolocation", newState, newPage, userContext.UserContext);
     
     var after = await window.Script.CallFunctionAsync("""
     async () => (await navigator.permissions.query({ name: "geolocation" })).state
     """, awaitPromise: true, new() { UserActivation = true });
     
    +Assert.That(after.Result.ToString(), Is.Not.EqualTo(initialState));
    +

    [To ensure code accuracy, apply this suggestion manually]

    Suggestion importance[1-10]: 7

    __

    Why: The suggestion improves test reliability by removing the assumption about the initial permission state being "prompt". Instead, it adapts the test to work with any initial state, making it more robust across different browser environments.

    Medium
    Add parameter validation

    The method doesn't validate the permissionName parameter, which could lead to
    runtime errors if null or empty values are passed. Add parameter validation to
    ensure the permissionName is not null or empty.

    dotnet/src/webdriver/BiDi/Extensions/Permissions/PermissionsModule.cs [36-41]

     public async Task SetPermissionAsync(string permissionName, PermissionState state, string origin, UserContext? userContext, SetPermissionOptions? options = null)
     {
    +    if (string.IsNullOrEmpty(permissionName))
    +    {
    +        throw new ArgumentException("Permission name cannot be null or empty", nameof(permissionName));
    +    }
    +    
    +    if (string.IsNullOrEmpty(origin))
    +    {
    +        throw new ArgumentException("Origin cannot be null or empty", nameof(origin));
    +    }
    +    
         var @params = new SetPermissionCommandParameters(new PermissionDescriptor(permissionName), state, origin, userContext);
     
         await Broker.ExecuteCommandAsync(new SetPermissionCommand(@params), options).ConfigureAwait(false);
     }
    • Apply this suggestion
    Suggestion importance[1-10]: 6

    __

    Why: The suggestion adds important input validation to prevent potential runtime errors when null or empty values are passed. This defensive programming practice improves code robustness and provides clearer error messages.

    Low
    • More

    @RenderMichael RenderMichael marked this pull request as draft March 22, 2025 05:47
    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.

    2 participants