diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..32fd1d2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,120 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +# Code files +[*.{cs,csx,vb,vbx}] +indent_style = space +indent_size = 4 +end_of_line = crlf + +# Project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# Razor files +[*.{cshtml,razor}] +indent_style = space +indent_size = 4 + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# C# Code Style Rules +[*.cs] + +# Code style rules +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Language rules +csharp_prefer_braces = true:warning +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_static_local_functions = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion + +# Formatting rules +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Naming conventions +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = warning +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = prefix_interface_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = warning +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.prefix_interface_with_i.required_prefix = I +dotnet_naming_style.prefix_interface_with_i.required_suffix = +dotnet_naming_style.prefix_interface_with_i.word_separator = +dotnet_naming_style.prefix_interface_with_i.capitalization = pascal_case + +# Code quality rules +dotnet_analyzer_diagnostic.category-security.severity = warning +dotnet_analyzer_diagnostic.category-reliability.severity = warning +dotnet_analyzer_diagnostic.category-performance.severity = suggestion \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28bfd8e..54e9556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,22 @@ jobs: if: matrix.language == 'csharp' && matrix.build-mode == 'manual' run: dotnet build RazorPagesMovie.sln --configuration Release --no-restore + - name: Run Code Analysis and Linting + if: matrix.language == 'csharp' && matrix.build-mode == 'manual' + run: | + echo "Running static analysis and linting checks..." + dotnet build RazorPagesMovie.sln --configuration Release --verbosity normal --no-restore | tee build-output.log + + # Count warnings from our static analysis tools + ANALYZER_WARNINGS=$(grep -c "warning SA\|warning CA" build-output.log || true) + echo "Static analysis warnings found: $ANALYZER_WARNINGS" + + # For now, we'll log the count but not fail the build to avoid breaking existing CI + # In a production environment, you might want to set thresholds or fail on new warnings + if [ "$ANALYZER_WARNINGS" -gt 0 ]; then + echo "::notice title=Static Analysis::Found $ANALYZER_WARNINGS static analysis warnings. Consider addressing them to improve code quality." + fi + # - name: Set runtime # if: matrix.language == 'csharp' # id: set-runtime diff --git a/src/Data/RazorPagesMovieContext.cs b/src/Data/RazorPagesMovieContext.cs index 2e453d7..a8e1202 100644 --- a/src/Data/RazorPagesMovieContext.cs +++ b/src/Data/RazorPagesMovieContext.cs @@ -16,6 +16,7 @@ public RazorPagesMovieContext(DbContextOptions options) public DbSet Users { get; set; } = default!; public DbSet Directors { get; set; } = default!; public DbSet Reviews { get; set; } = default!; + public DbSet Artists { get; set; } = default!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -27,6 +28,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Name).IsRequired().HasMaxLength(100); entity.Property(e => e.BirthDate).IsRequired(); }); + + // Artist configuration + modelBuilder.Entity(entity => + { + entity.Property(e => e.Name).IsRequired().HasMaxLength(100); + entity.Property(e => e.BirthDate).IsRequired(); + entity.Property(e => e.Nationality).HasMaxLength(50); + }); // User configuration modelBuilder.Entity(entity => diff --git a/src/Migrations/20241216230000_AddArtistTable.cs b/src/Migrations/20241216230000_AddArtistTable.cs new file mode 100644 index 0000000..a703d07 --- /dev/null +++ b/src/Migrations/20241216230000_AddArtistTable.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RazorPagesMovie.Migrations +{ + public partial class AddArtistTable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Artists", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + BirthDate = table.Column(type: "datetime2", nullable: false), + Nationality = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Timestamp = table.Column(type: "rowversion", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Artists", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Artists"); + } + } +} \ No newline at end of file diff --git a/src/Models/Artist.cs b/src/Models/Artist.cs new file mode 100644 index 0000000..fb816f6 --- /dev/null +++ b/src/Models/Artist.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RazorPagesMovie.Models +{ + public class Artist + { + public int Id { get; set; } + + [Required] + public string Name { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Date)] + public DateTime BirthDate { get; set; } + + public string? Nationality { get; set; } + + [Timestamp] + public byte[]? Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/RazorPagesMovie.csproj b/src/RazorPagesMovie.csproj index aab37e8..fb300b2 100644 --- a/src/RazorPagesMovie.csproj +++ b/src/RazorPagesMovie.csproj @@ -7,6 +7,15 @@ false false false + + + + + false + Recommended + true + true + true @@ -27,6 +36,16 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + \ No newline at end of file diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 0000000..3ea1f2b --- /dev/null +++ b/stylecop.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "documentExposedElements": false, + "documentInternalElements": false, + "documentPrivateElements": false, + "documentPrivateFields": false, + "documentInterfaces": false, + "documentExposedElements": false, + "companyName": "RazorPagesMovie", + "copyrightText": "Copyright (c) RazorPagesMovie. All rights reserved.\nLicensed under the MIT license." + }, + "orderingRules": { + "systemUsingDirectivesFirst": true, + "usingDirectivesPlacement": "insideNamespace" + }, + "namingRules": { + "allowCommonHungarianPrefixes": false, + "allowedHungarianPrefixes": [] + }, + "maintainabilityRules": { + "topLevelTypes": [] + }, + "layoutRules": { + "newlineAtEndOfFile": "require", + "allowConsecutiveUsings": true + }, + "readabilityRules": { + "allowBuiltInTypeAliases": true + } + } +} \ No newline at end of file diff --git a/tests/RazorPagesMovie.Tests/ArtistTests.cs b/tests/RazorPagesMovie.Tests/ArtistTests.cs new file mode 100644 index 0000000..92c452e --- /dev/null +++ b/tests/RazorPagesMovie.Tests/ArtistTests.cs @@ -0,0 +1,117 @@ +using System; +using Xunit; +using RazorPagesMovie.Models; +using Xunit.Abstractions; + +namespace RazorPagesMovie.Tests +{ + public class ArtistTests + { + private readonly ITestOutputHelper _output; + + public ArtistTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void CanChangeName() + { + // Arrange + var artist = new Artist { Name = "Old Name" }; + _output.WriteLine("=== Test Output ==="); + _output.WriteLine($"Initial Name: {artist.Name}"); + + // Act + artist.Name = "New Name"; + _output.WriteLine($"Updated Name: {artist.Name}"); + _output.WriteLine("==================="); + + // Assert + Assert.Equal("New Name", artist.Name); + Assert.NotEqual("Old Name", artist.Name); + Assert.False(string.IsNullOrEmpty(artist.Name)); + } + + [Fact] + public void CanChangeBirthDate() + { + // Arrange + var artist = new Artist { BirthDate = DateTime.Parse("1980-01-01") }; + _output.WriteLine("=== Test Output ==="); + _output.WriteLine($"Initial Birth Date: {artist.BirthDate}"); + + // Act + artist.BirthDate = DateTime.Parse("1985-12-31"); + _output.WriteLine($"Updated Birth Date: {artist.BirthDate}"); + _output.WriteLine("==================="); + + // Assert + Assert.Equal(DateTime.Parse("1985-12-31"), artist.BirthDate); + Assert.NotEqual(DateTime.Parse("1980-01-01"), artist.BirthDate); + Assert.True(artist.BirthDate > DateTime.MinValue); + } + + [Fact] + public void CanChangeNationality() + { + // Arrange + var artist = new Artist { Nationality = "American" }; + _output.WriteLine("=== Test Output ==="); + _output.WriteLine($"Initial Nationality: {artist.Nationality}"); + + // Act + artist.Nationality = "British"; + _output.WriteLine($"Updated Nationality: {artist.Nationality}"); + _output.WriteLine("==================="); + + // Assert + Assert.Equal("British", artist.Nationality); + Assert.NotEqual("American", artist.Nationality); + } + + [Theory] + [InlineData("Leonardo DiCaprio")] + [InlineData("Meryl Streep")] + [InlineData("Robert De Niro")] + public void CanChangeNameWithDifferentValues(string newName) + { + // Arrange + var artist = new Artist { Name = "Old Name" }; + _output.WriteLine("=== Test Output ==="); + _output.WriteLine($"Initial Name: {artist.Name}"); + + // Act + artist.Name = newName; + _output.WriteLine($"Updated Name: {artist.Name}"); + _output.WriteLine("==================="); + + // Assert + Assert.Equal(newName, artist.Name); + Assert.NotEqual("Old Name", artist.Name); + Assert.False(string.IsNullOrEmpty(artist.Name)); + } + + [Theory] + [InlineData("American")] + [InlineData("British")] + [InlineData("French")] + [InlineData(null)] + public void CanChangeNationalityWithDifferentValues(string? newNationality) + { + // Arrange + var artist = new Artist { Nationality = "German" }; + _output.WriteLine("=== Test Output ==="); + _output.WriteLine($"Initial Nationality: {artist.Nationality}"); + + // Act + artist.Nationality = newNationality; + _output.WriteLine($"Updated Nationality: {artist.Nationality}"); + _output.WriteLine("==================="); + + // Assert + Assert.Equal(newNationality, artist.Nationality); + Assert.NotEqual("German", artist.Nationality); + } + } +} \ No newline at end of file