diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..595f1e7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,287 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] + +trim_trailing_whitespace = true + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +# C# files +[*.cs] + +#### Analyzer / IDE diagnostics (grouped) #### + +# Headers +dotnet_diagnostic.IDE0073.severity = warning # File header mismatch + +# Usings +dotnet_diagnostic.IDE0005.severity = warning # Remove unused usings + +# var preferences (only if you want nudges) +dotnet_diagnostic.IDE0007.severity = suggestion # Use explicit type instead of 'var' +dotnet_diagnostic.IDE0008.severity = suggestion # Use explicit type instead of 'var' + +# Namespace/folder mismatch +dotnet_diagnostic.IDE0130.severity = suggestion # Namespace doesn't match folder structure + +# Unnecessary suppression +dotnet_diagnostic.IDE0079.severity = warning # Unnecessary suppression + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = true + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = true +dotnet_sort_system_directives_first = true + +# Generic Lost Minions file header +#file_header_template = Copyright (c) Lost Minions and contributors. All rights reserved.\nSee the LICENSE file in the project root for license information. + +# Prefer object/collection initializers (you already prefer them, make it visible) +dotnet_diagnostic.IDE0017.severity = suggestion +dotnet_diagnostic.IDE0028.severity = suggestion + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# Override for global usings files +[*.GlobalUsings.cs] +dotnet_diagnostic.IDE0005.severity = none diff --git a/.github/workflows/test-compile.yml b/.github/workflows/test-compile.yml new file mode 100644 index 0000000..aff5d37 --- /dev/null +++ b/.github/workflows/test-compile.yml @@ -0,0 +1,46 @@ +name: 🧪 C# Compilation Test + +on: + workflow_dispatch: + push: + branches: [ main, master ] + paths: + - ".github/workflows/test-compile.yml" + - "**/*.cs" + - "**/*.csproj" + - "*.sln" + +jobs: + compile-test: + runs-on: windows-latest + + env: + SOLUTION_NAME: Mee6LevelsAPI.sln + + steps: + - name: ⚙️ Configure Git + shell: pwsh + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global credential.helper store + + - name: 🧾 Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: 🧰 Setup .NET 8 SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: 🧱 Restore dependencies + run: dotnet restore $env:SOLUTION_NAME + + - name: 🏗️ Build solution + run: dotnet build $env:SOLUTION_NAME --configuration Release --no-restore + + - name: ✅ Verify Build Success + run: echo "✅ $env:SOLUTION_NAME compiled successfully on GitHub Actions." diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..eba7ce0 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + net8.0 + latest + enable + true + + diff --git a/Mee6LevelsAPI.Tests/Mee6DtoTests.cs b/Mee6LevelsAPI.Tests/Mee6DtoTests.cs new file mode 100644 index 0000000..73da45f --- /dev/null +++ b/Mee6LevelsAPI.Tests/Mee6DtoTests.cs @@ -0,0 +1,330 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; + +using Newtonsoft.Json; + +using Xunit; + +namespace Mee6LevelsAPI.Tests +{ + public sealed class Mee6DtoTests + { + [Fact] + public void Mee6UserInfo_JsonSerialization_RoundTripsFields() + { + Mee6UserInfo original = new() + { + Avatar = "avatar-hash", + DetailedXp = new[] { 10, 20, 30 }, + Discriminator = "1234", + GuildId = "987654321", + Id = "123456789", + Level = 7, + MessageCount = 42, + Username = "TestUser", + Xp = 12345 + }; + + string json = JsonConvert.SerializeObject(original); + Mee6UserInfo deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(original.Avatar, deserialized.Avatar); + Assert.Equal(original.DetailedXp, deserialized.DetailedXp); + Assert.Equal(original.Discriminator, deserialized.Discriminator); + Assert.Equal(original.GuildId, deserialized.GuildId); + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.Level, deserialized.Level); + Assert.Equal(original.MessageCount, deserialized.MessageCount); + Assert.Equal(original.Username, deserialized.Username); + Assert.Equal(original.Xp, deserialized.Xp); + } + + [Fact] + public void Mee6GuildInfo_JsonSerialization_RoundTripsFields() + { + Mee6GuildInfo original = new() + { + AllowJoin = true, + Icon = "icon-hash", + Id = "123456789", + InviteLeaderboard = true, + LeaderboardURL = "https://mee6.xyz/leaderboard/123456789", + Name = "Test Guild", + Premium = false + }; + + string json = JsonConvert.SerializeObject(original); + Mee6GuildInfo deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(original.AllowJoin, deserialized.AllowJoin); + Assert.Equal(original.Icon, deserialized.Icon); + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.InviteLeaderboard, deserialized.InviteLeaderboard); + Assert.Equal(original.LeaderboardURL, deserialized.LeaderboardURL); + Assert.Equal(original.Name, deserialized.Name); + Assert.Equal(original.Premium, deserialized.Premium); + } + + [Fact] + public void Mee6Role_JsonSerialization_RoundTripsFields() + { + Mee6Role original = new() + { + Color = 0x00FF00, + Hoist = true, + Id = "987654321", + Managed = false, + Mentionable = true, + Name = "Tester", + Permissions = 123456789L, + Position = 5 + }; + + string json = JsonConvert.SerializeObject(original); + Mee6Role deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(original.Color, deserialized.Color); + Assert.Equal(original.Hoist, deserialized.Hoist); + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.Managed, deserialized.Managed); + Assert.Equal(original.Mentionable, deserialized.Mentionable); + Assert.Equal(original.Name, deserialized.Name); + Assert.Equal(original.Permissions, deserialized.Permissions); + Assert.Equal(original.Position, deserialized.Position); + } + + [Fact] + public void Mee6RoleInfo_JsonSerialization_RoundTripsFields() + { + Mee6RoleInfo original = new() + { + Rank = 1, + Role = new Mee6Role + { + Color = 0xFF0000, + Hoist = false, + Id = "role-id", + Managed = true, + Mentionable = false, + Name = "RoleName", + Permissions = 777, + Position = 10 + } + }; + + string json = JsonConvert.SerializeObject(original); + Mee6RoleInfo deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(original.Rank, deserialized.Rank); + Assert.Equal(original.Role.Color, deserialized.Role.Color); + Assert.Equal(original.Role.Hoist, deserialized.Role.Hoist); + Assert.Equal(original.Role.Id, deserialized.Role.Id); + Assert.Equal(original.Role.Managed, deserialized.Role.Managed); + Assert.Equal(original.Role.Mentionable, deserialized.Role.Mentionable); + Assert.Equal(original.Role.Name, deserialized.Role.Name); + Assert.Equal(original.Role.Permissions, deserialized.Role.Permissions); + Assert.Equal(original.Role.Position, deserialized.Role.Position); + } + + [Fact] + public void Mee6Server_JsonSerialization_RoundTripsFields() + { + Mee6UserInfo user = new() + { + Avatar = "avatar-hash", + DetailedXp = new[] { 100, 200 }, + Discriminator = "0001", + GuildId = "guild-1", + Id = "user-1", + Level = 3, + MessageCount = 10, + Username = "User1", + Xp = 300 + }; + + Mee6RoleInfo roleInfo = new() + { + Rank = 1, + Role = new Mee6Role + { + Color = 1, + Hoist = false, + Id = "role-1", + Managed = false, + Mentionable = true, + Name = "Role1", + Permissions = 42, + Position = 1 + } + }; + + Mee6Server original = new() + { + Admin = true, + BannerURL = "https://example.com/banner.png", + Guild = new Mee6GuildInfo + { + AllowJoin = true, + Icon = "icon-hash", + Id = "guild-1", + InviteLeaderboard = false, + LeaderboardURL = "https://mee6.xyz/leaderboard/guild-1", + Name = "Guild One", + Premium = true + }, + IsMember = true, + Page = 1, + Users = new[] { user }, + RewardRoles = new[] { roleInfo }, + XPPerMessage = new[] { 5L, 10L }, + XPRate = 2 + }; + + string json = JsonConvert.SerializeObject(original); + Mee6Server deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(original.Admin, deserialized.Admin); + Assert.Equal(original.BannerURL, deserialized.BannerURL); + + Assert.Equal(original.Guild.AllowJoin, deserialized.Guild.AllowJoin); + Assert.Equal(original.Guild.Icon, deserialized.Guild.Icon); + Assert.Equal(original.Guild.Id, deserialized.Guild.Id); + Assert.Equal(original.Guild.InviteLeaderboard, deserialized.Guild.InviteLeaderboard); + Assert.Equal(original.Guild.LeaderboardURL, deserialized.Guild.LeaderboardURL); + Assert.Equal(original.Guild.Name, deserialized.Guild.Name); + Assert.Equal(original.Guild.Premium, deserialized.Guild.Premium); + + Assert.Equal(original.IsMember, deserialized.IsMember); + Assert.Equal(original.Page, deserialized.Page); + + Assert.NotNull(deserialized.Users); + _ = Assert.Single(deserialized.Users); + Assert.Equal(original.Users[0].Id, deserialized.Users[0].Id); + Assert.Equal(original.Users[0].Username, deserialized.Users[0].Username); + Assert.Equal(original.Users[0].Xp, deserialized.Users[0].Xp); + + Assert.NotNull(deserialized.RewardRoles); + _ = Assert.Single(deserialized.RewardRoles); + Assert.Equal(original.RewardRoles[0].Rank, deserialized.RewardRoles[0].Rank); + Assert.Equal(original.RewardRoles[0].Role.Name, deserialized.RewardRoles[0].Role.Name); + + Assert.Equal(original.XPPerMessage, deserialized.XPPerMessage); + Assert.Equal(original.XPRate, deserialized.XPRate); + } + + [Theory] + [InlineData(typeof(Mee6UserInfo), "Avatar", "avatar")] + [InlineData(typeof(Mee6UserInfo), "DetailedXp", "detailed_xp")] + [InlineData(typeof(Mee6UserInfo), "Discriminator", "discriminator")] + [InlineData(typeof(Mee6UserInfo), "GuildId", "guild_id")] + [InlineData(typeof(Mee6UserInfo), "Id", "id")] + [InlineData(typeof(Mee6UserInfo), "Level", "level")] + [InlineData(typeof(Mee6UserInfo), "MessageCount", "message_count")] + [InlineData(typeof(Mee6UserInfo), "Username", "username")] + [InlineData(typeof(Mee6UserInfo), "Xp", "xp")] + public void Mee6UserInfo_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName) + { + FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(field); + + JsonPropertyAttribute? attr = field!.GetCustomAttribute(); + Assert.NotNull(attr); + Assert.Equal(expectedJsonName, attr!.PropertyName); + } + + [Theory] + [InlineData(typeof(Mee6GuildInfo), "AllowJoin", "allow_join")] + [InlineData(typeof(Mee6GuildInfo), "Icon", "icon")] + [InlineData(typeof(Mee6GuildInfo), "Id", "id")] + [InlineData(typeof(Mee6GuildInfo), "InviteLeaderboard", "invite_leaderboard")] + [InlineData(typeof(Mee6GuildInfo), "LeaderboardURL", "leaderboard_url")] + [InlineData(typeof(Mee6GuildInfo), "Name", "name")] + [InlineData(typeof(Mee6GuildInfo), "Premium", "premium")] + public void Mee6GuildInfo_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName) + { + FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(field); + + JsonPropertyAttribute? attr = field!.GetCustomAttribute(); + Assert.NotNull(attr); + Assert.Equal(expectedJsonName, attr!.PropertyName); + } + + [Theory] + [InlineData(typeof(Mee6Role), "Color", "color")] + [InlineData(typeof(Mee6Role), "Hoist", "hoist")] + [InlineData(typeof(Mee6Role), "Id", "id")] + [InlineData(typeof(Mee6Role), "Managed", "managed")] + [InlineData(typeof(Mee6Role), "Mentionable", "mentionable")] + [InlineData(typeof(Mee6Role), "Name", "name")] + [InlineData(typeof(Mee6Role), "Permissions", "permissions")] + [InlineData(typeof(Mee6Role), "Position", "position")] + public void Mee6Role_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName) + { + FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(field); + + JsonPropertyAttribute? attr = field!.GetCustomAttribute(); + Assert.NotNull(attr); + Assert.Equal(expectedJsonName, attr!.PropertyName); + } + + [Theory] + [InlineData(typeof(Mee6Server), "Admin", "admin")] + [InlineData(typeof(Mee6Server), "BannerURL", "banner_url")] + [InlineData(typeof(Mee6Server), "Guild", "guild")] + [InlineData(typeof(Mee6Server), "IsMember", "is_member")] + [InlineData(typeof(Mee6Server), "Page", "page")] + [InlineData(typeof(Mee6Server), "Users", "players")] + [InlineData(typeof(Mee6Server), "RewardRoles", "role_rewards")] + [InlineData(typeof(Mee6Server), "XPPerMessage", "xp_per_message")] + [InlineData(typeof(Mee6Server), "XPRate", "xp_rate")] + public void Mee6Server_HasExpectedJsonPropertyAttributes(Type type, string fieldName, string expectedJsonName) + { + FieldInfo? field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(field); + + JsonPropertyAttribute? attr = field!.GetCustomAttribute(); + Assert.NotNull(attr); + Assert.Equal(expectedJsonName, attr!.PropertyName); + } + } + + // ---------------------------------------------------------------------- + // NOTE: The methods below (Mee6.GetServer / GetServerAsync / GetUserInfo / + // GetAvatarAsync) create HttpClient internally and hit the real Mee6 / + // Discord APIs. To truly unit test them, you’ll want to refactor Mee6 to + // accept an HttpClient (or an IMee6Api abstraction) so you can inject a + // fake handler in tests. + // + // For now, you could write integration tests like this, replacing the + // IDs with real ones from your server: + // ---------------------------------------------------------------------- + public sealed class Mee6IntegrationExamples + { + // Replace with a real guild ID that has Mee6 levels enabled. + private const long ExampleGuildId = 123456789012345678; + + // Replace with a real user ID in that guild. + private const long ExampleUserId = 234567890123456789; + + [Fact(Skip = "Integration example – hits the real Mee6 API; configure IDs before enabling.")] + public async Task GetServerAsync_ReturnsUsers_ForRealGuild() + { + Mee6Server server = await Mee6.GetServerAsync(ExampleGuildId); + + Assert.NotNull(server.Users); + Assert.NotEmpty(server.Users); + } + + [Fact(Skip = "Integration example – hits the real Mee6 API; configure IDs before enabling.")] + public void GetUserInfo_ReturnsUser_ForRealGuildAndUser() + { + Mee6UserInfo user = Mee6.GetUserInfo(ExampleGuildId, ExampleUserId); + + Assert.False(string.IsNullOrEmpty(user.Id)); + Assert.False(string.IsNullOrEmpty(user.Username)); + } + } +} diff --git a/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj b/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj new file mode 100644 index 0000000..df61d0a --- /dev/null +++ b/Mee6LevelsAPI.Tests/Mee6LevelsAPI.Tests.csproj @@ -0,0 +1,31 @@ + + + + + Mee6LevelsAPI.Tests + Mee6LevelsAPI.Tests + + + false + + + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/Mee6LevelsAPI.sln b/Mee6LevelsAPI.sln index e894acb..1b5599d 100644 --- a/Mee6LevelsAPI.sln +++ b/Mee6LevelsAPI.sln @@ -1,10 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30717.126 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36518.9 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mee6LevelsAPI", "Mee6LevelsAPI\Mee6LevelsAPI.csproj", "{AC3BDAED-B576-425E-8893-EDDB796C214F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mee6LevelsAPI.Tests", "Mee6LevelsAPI.Tests\Mee6LevelsAPI.Tests.csproj", "{FD769813-41E0-A579-9F32-110065E5073D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,10 +19,17 @@ Global {AC3BDAED-B576-425E-8893-EDDB796C214F}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC3BDAED-B576-425E-8893-EDDB796C214F}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC3BDAED-B576-425E-8893-EDDB796C214F}.Release|Any CPU.Build.0 = Release|Any CPU + {FD769813-41E0-A579-9F32-110065E5073D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD769813-41E0-A579-9F32-110065E5073D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD769813-41E0-A579-9F32-110065E5073D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD769813-41E0-A579-9F32-110065E5073D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FD769813-41E0-A579-9F32-110065E5073D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC28C2AB-FAB9-4442-8812-48ADFB08FA3D} EndGlobalSection diff --git a/Mee6LevelsAPI/Mee6.cs b/Mee6LevelsAPI/Mee6.cs index 402061b..7386cde 100644 --- a/Mee6LevelsAPI/Mee6.cs +++ b/Mee6LevelsAPI/Mee6.cs @@ -1,25 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Drawing; -using System.Net.Http; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.IO; +using Newtonsoft.Json; + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Mee6LevelsAPI { public static class Mee6 { - const string Url = "https://mee6.xyz/api/plugins/levels/leaderboard/"; + private const string Url = "https://mee6.xyz/api/plugins/levels/leaderboard/"; public static int Limit = 1000; + public static Mee6UserInfo GetUserInfo(long guildID, long userID) { Mee6Server server = GetServer(guildID); - for(int i = 0; i < Limit; i++) + for (int i = 0; i < Limit; i++) { Mee6UserInfo user = server.Users[i]; if (user.Id.Equals(userID.ToString())) @@ -32,9 +27,9 @@ public static Mee6UserInfo GetUserInfo(long guildID, long userID) public static Mee6Server GetServer(long guildID) { - HttpClient client = new HttpClient(); + HttpClient client = new(); HttpResponseMessage response = client.GetAsync(Url + $"{guildID}?limit={Limit}").Result; - response.EnsureSuccessStatusCode(); + _ = response.EnsureSuccessStatusCode(); string responseBody = response.Content.ReadAsStringAsync().Result; Mee6Server server = JsonConvert.DeserializeObject(responseBody); @@ -43,9 +38,9 @@ public static Mee6Server GetServer(long guildID) public static async Task GetServerAsync(long guildID) { - HttpClient client = new HttpClient(); + HttpClient client = new(); HttpResponseMessage response = await client.GetAsync(Url + $"{guildID}?limit={Limit}"); - response.EnsureSuccessStatusCode(); + _ = response.EnsureSuccessStatusCode(); string responseBody = await response.Content.ReadAsStringAsync(); Mee6Server server = JsonConvert.DeserializeObject(responseBody); @@ -54,9 +49,9 @@ public static async Task GetServerAsync(long guildID) public static Mee6Server GetServer(long guildID, string fileName) { - HttpClient client = new HttpClient(); + HttpClient client = new(); HttpResponseMessage response = client.GetAsync(Url + $"{guildID}?limit={Limit}").Result; - response.EnsureSuccessStatusCode(); + _ = response.EnsureSuccessStatusCode(); string responseBody = response.Content.ReadAsStringAsync().Result; Mee6Server server = JsonConvert.DeserializeObject(responseBody); @@ -65,16 +60,12 @@ public static Mee6Server GetServer(long guildID, string fileName) return server; } - public static Image GetAvatar(Mee6UserInfo user, int size) + public static async Task> GetAvatarAsync(Mee6UserInfo user, int size) { string imageUrl = $"https://cdn.discordapp.com/avatars/{user.Id}/{user.Avatar}?size={size}"; - using (System.Net.WebClient webClient = new System.Net.WebClient()) - { - using (Stream stream = webClient.OpenRead(imageUrl)) - { - return Image.FromStream(stream); - } - } + using HttpClient httpClient = new(); + using Stream stream = await httpClient.GetStreamAsync(imageUrl); + return await Image.LoadAsync(stream); } } @@ -82,20 +73,28 @@ public struct Mee6UserInfo { [JsonProperty("avatar")] public string Avatar; + [JsonProperty("detailed_xp")] public int[] DetailedXp; + [JsonProperty("discriminator")] public string Discriminator; + [JsonProperty("guild_id")] public string GuildId; + [JsonProperty("id")] public string Id; + [JsonProperty("level")] public long Level; + [JsonProperty("message_count")] public long MessageCount; + [JsonProperty("username")] public string Username; + [JsonProperty("xp")] public long Xp; } @@ -104,16 +103,22 @@ public struct Mee6GuildInfo { [JsonProperty("allow_join")] public bool AllowJoin; + [JsonProperty("icon")] public string Icon; + [JsonProperty("id")] public string Id; + [JsonProperty("invite_leaderboard")] public bool InviteLeaderboard; + [JsonProperty("leaderboard_url")] public string LeaderboardURL; + [JsonProperty("name")] public string Name; + [JsonProperty("premium")] public bool Premium; } @@ -122,6 +127,7 @@ public struct Mee6RoleInfo { [JsonProperty("rank")] public long Rank; + [JsonProperty("role")] public Mee6Role Role; } @@ -130,18 +136,25 @@ public struct Mee6Role { [JsonProperty("color")] public long Color; + [JsonProperty("hoist")] public bool Hoist; + [JsonProperty("id")] public string Id; + [JsonProperty("managed")] public bool Managed; + [JsonProperty("mentionable")] public bool Mentionable; + [JsonProperty("name")] public string Name; + [JsonProperty("permissions")] public long Permissions; + [JsonProperty("position")] public long Position; } @@ -150,22 +163,30 @@ public struct Mee6Server { [JsonProperty("admin")] public bool Admin; + [JsonProperty("banner_url")] public string BannerURL; + [JsonProperty("guild")] public Mee6GuildInfo Guild; + [JsonProperty("is_member")] public bool IsMember; + [JsonProperty("page")] public long Page; + [JsonProperty("players")] public Mee6UserInfo[] Users; + [JsonProperty("role_rewards")] public Mee6RoleInfo[] RewardRoles; + //user_guild_settings not implemented [JsonProperty("xp_per_message")] public long[] XPPerMessage; + [JsonProperty("xp_rate")] public long XPRate; } -} +} \ No newline at end of file diff --git a/Mee6LevelsAPI/Mee6LevelsAPI.csproj b/Mee6LevelsAPI/Mee6LevelsAPI.csproj index 5d791b3..cf78285 100644 --- a/Mee6LevelsAPI/Mee6LevelsAPI.csproj +++ b/Mee6LevelsAPI/Mee6LevelsAPI.csproj @@ -1,55 +1,24 @@ - - - + + - Debug - AnyCPU - {AC3BDAED-B576-425E-8893-EDDB796C214F} - Library - Properties - Mee6LevelsAPI + + enable + enable + Mee6LevelsAPI - v4.7.2 - 512 - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + Mee6LevelsAPI + Library + latest + - - ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - + + + + + + + - - \ No newline at end of file + + diff --git a/Mee6LevelsAPI/Properties/AssemblyInfo.cs b/Mee6LevelsAPI/Properties/AssemblyInfo.cs deleted file mode 100644 index 1c5ac2a..0000000 --- a/Mee6LevelsAPI/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Mee6LevelsAPI")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Mee6LevelsAPI")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ac3bdaed-b576-425e-8893-eddb796c214f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Mee6LevelsAPI/packages.config b/Mee6LevelsAPI/packages.config deleted file mode 100644 index a9de8b5..0000000 --- a/Mee6LevelsAPI/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file