diff --git a/Source/SmartMigrations/MigrationAttribute.cs b/Source/SmartMigrations/MigrationAttribute.cs index ff244e1..472c8e8 100644 --- a/Source/SmartMigrations/MigrationAttribute.cs +++ b/Source/SmartMigrations/MigrationAttribute.cs @@ -6,19 +6,6 @@ namespace SmartMigrations; [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public sealed class MigrationAttribute : Attribute { - // The static method holding checks for the shared migration data validity - private static MigrationAttribute CreateMigrationAttribute( - int[] fromList, - bool isRange, - int to, - string? fromSchema, - string? toSchema, - bool shouldAvoid - ) - { - return new MigrationAttribute(fromList, isRange, to, fromSchema, toSchema, shouldAvoid); - } - /// /// Gets the list of versions this migration can migrate from. /// @@ -34,10 +21,16 @@ bool shouldAvoid /// public int ToVersion { get; } - // TODO NEeds docs + /// + /// Gets the schema this migration can migrate from. Null indicates the default schema. + /// Used for cross-schema migration capability. + /// public string? FromSchema { get; } - // TODO Needs docs + /// + /// Gets the schema this migration migrates to. Null indicates the default schema. + /// Used for cross-schema migration capability. + /// public string? ToSchema { get; } /// @@ -46,13 +39,17 @@ bool shouldAvoid /// public bool ShouldAvoid { get; } - + /// + /// A private constructor that takes all available values and performs the validation. + /// Its purpose is to be used by other, more user-friendly constructors. + /// + /// When provided values were invalid private MigrationAttribute( + string? fromSchema, int[] fromList, bool isRange, - int to, - string? fromSchema, string? toSchema, + int to, bool shouldAvoid ) { @@ -67,85 +64,188 @@ bool shouldAvoid if (fromList.Contains(to)) throw new ArgumentException("TODO To can't be same as from", nameof(to)); - fromSchema = fromSchema?.Trim(); - if (fromSchema != null && string.IsNullOrWhiteSpace(fromSchema)) + FromSchema = fromSchema?.Trim(); + if (FromSchema != null && string.IsNullOrWhiteSpace(FromSchema)) throw new ArgumentException("TODO from schema must be defined (null is a valid value)", nameof(fromSchema)); - toSchema = toSchema?.Trim(); - if (toSchema != null && string.IsNullOrWhiteSpace(toSchema)) + ToSchema = toSchema?.Trim(); + if (ToSchema != null && string.IsNullOrWhiteSpace(ToSchema)) throw new ArgumentException("TODO to schema must be defined (null is a valid value)", nameof(toSchema)); FromVersions = fromList; + if (FromVersions.Length == 0 && FromSchema != ToSchema) + { + throw new ArgumentException("Migration setting up schema from scratch can't have different 'from' and 'to' schemas.", nameof(fromList)); + } + IsRange = isRange; ToVersion = to; - FromSchema = fromSchema; - ToSchema = toSchema; ShouldAvoid = shouldAvoid; } + /// + /// Initializes a new instance of the class for setting up a specific schema from scratch. + /// This constructor creates a migration that can be applied when no version exists in the specified schema (initial setup migration). + /// + /// The schema this migration operates within. Null indicates the default schema. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + public MigrationAttribute(string? schema, int to, bool shouldAvoid = false) + : this(schema, [], false, schema, to, shouldAvoid) {} /// /// Initializes a new instance of the class for setting up the database schema from scratch. /// This constructor creates a migration that can be applied when no version exists in the database (initial setup migration). + /// Uses the default schema. /// /// The version this migration migrates to. /// Whether this migration should be avoided if alternatives exist. Default is false. public MigrationAttribute(int to, bool shouldAvoid = false) - : this([], false, to, null, null, shouldAvoid) {} + : this(null, [], false, null, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class with cross-schema migration capability. + /// Allows migration from one schema to another, enabling schema transitions. + /// + /// The schema this migration can migrate from. Null indicates the default schema. + /// The version this migration can migrate from. + /// The schema this migration migrates to. Null indicates the default schema. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when and are the same value. + public MigrationAttribute(string? fromSchema, int from, string? toSchema, int to, bool shouldAvoid = false) + : this(fromSchema, [from], false, toSchema, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class within a single schema. + /// + /// The schema this migration operates within. Null indicates the default schema. + /// The version this migration can migrate from. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when and are the same value. + public MigrationAttribute(string? schema, int from, int to, bool shouldAvoid = false) + : this(schema, [from], false, schema, to, shouldAvoid) {} /// /// Initializes a new instance of the class with a single source version. + /// Uses the default schema. /// /// The version this migration can migrate from. /// The version this migration migrates to. /// Whether this migration should be avoided if alternatives exist. Default is false. /// Thrown when and are the same value. public MigrationAttribute(int from, int to, bool shouldAvoid = false) - : this([from], false, to, null, null, shouldAvoid) {} + : this(null, [from], false, null, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class with multiple source versions and cross-schema capability. + /// + /// The schema this migration can migrate from. Null indicates the default schema. + /// The array of versions this migration can migrate from. + /// The schema this migration migrates to. Null indicates the default schema. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when is null. + /// Thrown when contains the same version as . + public MigrationAttribute(string? fromSchema, int[] fromList, string? toSchema, int to, bool shouldAvoid = false) + : this(fromSchema, fromList, false, toSchema, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class with multiple source versions within a single schema. + /// + /// The schema this migration operates within. Null indicates the default schema. + /// The array of versions this migration can migrate from. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when is null. + /// Thrown when contains the same version as . + public MigrationAttribute(string? schema, int[] fromList, int to, bool shouldAvoid = false) + : this(schema, fromList, false, schema, to, shouldAvoid) {} /// /// Initializes a new instance of the class with multiple source versions. + /// Uses the default schema. /// - /// The array of versions this migration can migrate from. Cannot be null. + /// The array of versions this migration can migrate from. /// The version this migration migrates to. /// Whether this migration should be avoided if alternatives exist. Default is false. /// Thrown when is null. /// Thrown when contains the same version as . public MigrationAttribute(int[] fromList, int to, bool shouldAvoid = false) - : this(fromList, false, to, null, null, shouldAvoid) {} + : this(null, fromList, false, null, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class with a range of source versions and cross-schema capability. + /// + /// The schema this migration can migrate from. Null indicates the default schema. + /// The start of the inclusive range of versions this migration can migrate from. + /// The end of the inclusive range of versions this migration can migrate from. + /// The schema this migration migrates to. Null indicates the default schema. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when range start is greater than range end, or when is within the range. + public MigrationAttribute(string? fromSchema, int fromRangeStart, int fromRangeEnd, string? toSchema, int to, bool shouldAvoid = false) + : this(fromSchema, [fromRangeStart, fromRangeEnd], true, toSchema, to, shouldAvoid) {} + + /// + /// Initializes a new instance of the class with a range of source versions within a single schema. + /// + /// The schema this migration operates within. Null indicates the default schema. + /// The start of the inclusive range of versions this migration can migrate from. + /// The end of the inclusive range of versions this migration can migrate from. + /// The version this migration migrates to. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when range start is greater than range end, or when is within the range. + public MigrationAttribute(string? schema, int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid = false) + : this(schema, [fromRangeStart, fromRangeEnd], true, schema, to, shouldAvoid) {} /// /// Initializes a new instance of the class with a range of source versions. + /// Uses the default schema. /// /// The start of the inclusive range of versions this migration can migrate from. /// The end of the inclusive range of versions this migration can migrate from. /// The version this migration migrates to. /// Whether this migration should be avoided if alternatives exist. Default is false. - /// Thrown when range start is greater than the range end, or when is within the range. + /// Thrown when range start is greater than range end, or when is within the range. public MigrationAttribute(int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid = false) - : this([fromRangeStart, fromRangeEnd], true, to, null, null, shouldAvoid) {} + : this(null, [fromRangeStart, fromRangeEnd], true, null, to, shouldAvoid) {} /// - /// Initializes a new instance of the class with string-based version specification. + /// Initializes a new instance of the class with string-based version specification and cross-schema capability. /// Supports single version ("12"), comma-separated list ("3, 6, 12, 7"), or range ("5..10"). /// + /// The schema this migration can migrate from. Null indicates the default schema. /// The version specification this migration can migrate from. Can be a single version, comma-separated list, or range. Use null to indicate this migration can be applied from any version. + /// The schema this migration migrates to. Null indicates the default schema. /// The version this migration migrates to as a string. /// Whether this migration should be avoided if alternatives exist. Default is false. /// Thrown when is null. - /// Thrown when or contains an invalid version format, is empty/whitespace, when from and to versions are the same, when to version is within a from range, or when a from list contains the to version. - public MigrationAttribute(string? from, string to, bool shouldAvoid = false) + /// Thrown when schema parameters are empty/whitespace, or contains an invalid version format or is empty/whitespace, when from and to versions are the same, when to version is within a from range, or when a from list contains the to version. + public MigrationAttribute(string? fromSchema, string? from, string? toSchema, string to, bool shouldAvoid = false) { - ArgumentNullException.ThrowIfNull(to); + // Schemas + FromSchema = fromSchema?.Trim(); + if (FromSchema != null && string.IsNullOrWhiteSpace(FromSchema)) + throw new ArgumentException("TODO from schema must be defined (null is a valid value)", nameof(fromSchema)); + ToSchema = toSchema?.Trim(); + if (ToSchema != null && string.IsNullOrWhiteSpace(ToSchema)) + throw new ArgumentException("TODO to schema must be defined (null is a valid value)", nameof(toSchema)); + // To + ArgumentNullException.ThrowIfNull(to); if (string.IsNullOrWhiteSpace(to)) throw new ArgumentException("To version cannot be empty or whitespace.", nameof(to)); - if (!int.TryParse(to.Trim(), out var toVersion)) throw new ArgumentException("To version must be an integer.", nameof(to)); - ToVersion = toVersion; + + // Should avoid ShouldAvoid = shouldAvoid; + + // From & IsRange + IsRange = false; // Handle null "from" @@ -192,12 +292,12 @@ public MigrationAttribute(string? from, string to, bool shouldAvoid = false) .Select(part => { if (!int.TryParse(part, out var version)) - throw new ArgumentException("All versions in the list must be an integers.", + throw new ArgumentException("All versions in the list must be integers.", nameof(from)); return version; }) .Distinct() - .ToList(); + .ToArray(); if (FromVersions.Contains(ToVersion)) throw new ArgumentException("From version list cannot contain the same version as the to version.", nameof(from)); @@ -211,5 +311,35 @@ public MigrationAttribute(string? from, string to, bool shouldAvoid = false) throw new ArgumentException("From version cannot be the same as to version.", nameof(from)); FromVersions = [version]; } + + if (FromVersions.Length == 0 && FromSchema != ToSchema) + { + throw new ArgumentException("Migration setting up schema from scratch can't have different 'from' and 'to' schemas.", nameof(from)); + } } + + /// + /// Initializes a new instance of the class with string-based version specification within a single schema. + /// Supports single version ("12"), comma-separated list ("3, 6, 12, 7"), or range ("5..10"). + /// + /// The schema this migration operates within. Null indicates the default schema. + /// The version specification this migration can migrate from. Can be a single version, comma-separated list, or range. Use null to indicate this migration can be applied from any version. + /// The version this migration migrates to as a string. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when is null. + /// Thrown when schema parameter is empty/whitespace, or contains an invalid version format or is empty/whitespace, when from and to versions are the same, when to version is within a from range, or when a from list contains the to version. + public MigrationAttribute(string? schema, string? from, string to, bool shouldAvoid = false) + : this(schema, from, schema, to, shouldAvoid) { } + + /// + /// Initializes a new instance of the class with string-based version specification in the default schema. + /// Supports single version ("12"), comma-separated list ("3, 6, 12, 7"), or range ("5..10"). + /// + /// The version specification this migration can migrate from. Can be a single version, comma-separated list, or range. Use null to indicate this migration can be applied from any version. + /// The version this migration migrates to as a string. + /// Whether this migration should be avoided if alternatives exist. Default is false. + /// Thrown when is null. + /// Thrown when or contains an invalid version format or is empty/whitespace, when from and to versions are the same, when to version is within a from range, or when a from list contains the to version. + public MigrationAttribute(string? from, string to, bool shouldAvoid = false) + : this(null, from, null, to, shouldAvoid) { } } diff --git a/Source/SmartMigrations/SmartMigrations.csproj b/Source/SmartMigrations/SmartMigrations.csproj index 494393a..8796593 100644 --- a/Source/SmartMigrations/SmartMigrations.csproj +++ b/Source/SmartMigrations/SmartMigrations.csproj @@ -10,5 +10,9 @@ database;migration;version; + + + + diff --git a/Tests/SmartMigrations.Test/MigrationAttributeListFromTests.cs b/Tests/SmartMigrations.Test/MigrationAttributeListFromTests.cs new file mode 100644 index 0000000..9e6260a --- /dev/null +++ b/Tests/SmartMigrations.Test/MigrationAttributeListFromTests.cs @@ -0,0 +1,161 @@ +namespace SmartMigrations.Test; + +using SmartMigrations; +using Xunit; + +public class MigrationAttributeListFromTests +{ + #region Constructor: MigrationAttribute(int[] fromList, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(new int[] { 1 }, 5, false, new int[] { 1 }, 5, false, false)] + [InlineData(new int[] { 1, 3, 5 }, 10, true, new int[] { 1, 3, 5 }, 10, false, true)] + [InlineData(new int[] { 0, 1, 2 }, 3, false, new int[] { 0, 1, 2 }, 3, false, false)] + [InlineData(new int[] { 10, 20, 30, 40 }, 50, true, new int[] { 10, 20, 30, 40 }, 50, false, true)] + [InlineData(new int[] { 100 }, 200, false, new int[] { 100 }, 200, false, false)] + [InlineData(new int[] { 5, 1, 3 }, 10, false, new int[] { 5, 1, 3 }, 10, false, false)] + [InlineData(new int[] { 5, 1, 3, 1, 5 }, 10, true, new int[] { 5, 1, 3 }, 10, false, true)] + public void Constructor_DefaultSchema_ListFrom_Success(int[] fromList, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(fromList, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Null(attribute.FromSchema); + Assert.Null(attribute.ToSchema); + } + + [Theory] + [InlineData(new int[] { 1, 5 }, 5, false)] + [InlineData(new int[] { 7, -12 }, 7, false)] + [InlineData(new int[] { 7, -12 }, -12, true)] + [InlineData(new int[] { 10, 20, 30 }, 20, true)] + [InlineData(new int[] { 5 }, 5, false)] + public void Constructor_DefaultSchema_ListFrom_ContainsTo_ThrowsException(int[] fromList, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromList, to, shouldAvoid)); + } + + [Fact] + public void Constructor_DefaultSchema_ListFrom_NullList_ThrowsException() + { + Assert.Throws(() => new MigrationAttribute((int[])null!, 5)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? schema, int[] fromList, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, new int[] { 1 }, 5, false, new int[] { 1 }, 5, false, false)] + [InlineData(null, new int[] { 1 }, 5, true, new int[] { 1 }, 5, false, true)] + [InlineData("free", new int[] { 1, 2, 3 }, 10, false, new int[] { 1, 2, 3 }, 10, false, false)] + [InlineData("free", new int[] { 1, 2, 3 }, 10, true, new int[] { 1, 2, 3 }, 10, false, true)] + [InlineData("paid", new int[] { 5, 10, 15, 20 }, 25, false, new int[] { 5, 10, 15, 20 }, 25, false, false)] + [InlineData("paid", new int[] { 5, 10, 15, 20 }, 25, true, new int[] { 5, 10, 15, 20 }, 25, false, true)] + [InlineData("enterprise", new int[] { -10, -5, 0, 5, 10 }, 15, false, new int[] { -10, -5, 0, 5, 10 }, 15, false, false)] + [InlineData("enterprise", new int[] { -10, -5, 0, 5, 10 }, 15, true, new int[] { -10, -5, 0, 5, 10 }, 15, false, true)] + public void Constructor_SpecificSchema_ListFrom_Success(string? schema, int[] fromList, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(schema, fromList, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(schema, attribute.FromSchema); + Assert.Equal(schema, attribute.ToSchema); + } + + [Theory] + [InlineData("", new int[] { 1 }, 5, false)] + [InlineData(" ", new int[] { 1 }, 5, false)] + [InlineData("\t", new int[] { 1 }, 5, false)] + [InlineData("\n", new int[] { 1 }, 5, false)] + public void Constructor_SpecificSchema_ListFrom_InvalidSchema_ThrowsException(string schema, int[] fromList, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(schema, fromList, to, shouldAvoid)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? fromSchema, int[] fromList, string? toSchema, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, new int[] { 1 }, null, 5, false, new int[] { 1 }, 5, false, false)] + [InlineData(null, new int[] { 1 }, null, 5, true, new int[] { 1 }, 5, false, true)] + [InlineData("free", new int[] { 1, 2, 3 }, "free", 10, false, new int[] { 1, 2, 3 }, 10, false, false)] + [InlineData("free", new int[] { 1, 2, 3 }, "free", 10, true, new int[] { 1, 2, 3 }, 10, false, true)] + [InlineData("free", new int[] { 5, 10, 15 }, "paid", 1, false, new int[] { 5, 10, 15 }, 1, false, false)] + [InlineData("free", new int[] { 5, 10, 15 }, "paid", 1, true, new int[] { 5, 10, 15 }, 1, false, true)] + [InlineData("paid", new int[] { 10, 20 }, "enterprise", 1, false, new int[] { 10, 20 }, 1, false, false)] + [InlineData("paid", new int[] { 10, 20 }, "enterprise", 1, true, new int[] { 10, 20 }, 1, false, true)] + [InlineData(null, new int[] { 5, 10 }, "premium", 1, false, new int[] { 5, 10 }, 1, false, false)] + [InlineData("basic", new int[] { 5, 10 }, null, 15, false, new int[] { 5, 10 }, 15, false, false)] + public void Constructor_CrossSchema_ListFrom_Success(string? fromSchema, int[] fromList, string? toSchema, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(fromSchema, fromList, toSchema, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(fromSchema, attribute.FromSchema); + Assert.Equal(toSchema, attribute.ToSchema); + } + + [Theory] + [InlineData("", new int[] { 1 }, "paid", 5, false)] + [InlineData(" ", new int[] { 1 }, "paid", 5, false)] + [InlineData("free", new int[] { 1 }, "", 5, false)] + [InlineData("free", new int[] { 1 }, " ", 5, false)] + [InlineData("\t", new int[] { 1 }, "paid", 5, false)] + [InlineData("free", new int[] { 1 }, "\n", 5, false)] + public void Constructor_CrossSchema_ListFrom_InvalidSchema_ThrowsException(string fromSchema, int[] fromList, string toSchema, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromSchema, fromList, toSchema, to, shouldAvoid)); + } + + #endregion + + #region Edge Cases for List From Migrations + + [Fact] + public void Constructor_ListFrom_DuplicatesRemoved() + { + var attribute = new MigrationAttribute(new int[] { 1, 2, 2, 3, 1, 3 }, 10); + Assert.Equal(new int[] { 1, 2, 3 }, attribute.FromVersions); + } + + [Fact] + public void Constructor_ListFrom_EmptyList() + { + var attribute = new MigrationAttribute(new int[] { }, 10); + Assert.Empty(attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + } + + [Fact] + public void Constructor_ListFrom_IsRangeFalse() + { + var attribute = new MigrationAttribute(new int[] { 1, 2, 3 }, 10); + Assert.False(attribute.IsRange); + } + + [Fact] + public void Constructor_ListFrom_OrderPreserved() + { + var attribute = new MigrationAttribute(new int[] { 5, 1, 3, 2 }, 10); + Assert.Equal(new int[] { 5, 1, 3, 2 }, attribute.FromVersions); + } + + [Fact] + public void Constructor_ListFrom_LargeList() + { + var largeList = Enumerable.Range(1, 100).ToArray(); + var attribute = new MigrationAttribute(largeList, 200); + Assert.Equal(largeList, attribute.FromVersions); + Assert.Equal(200, attribute.ToVersion); + } + + #endregion +} \ No newline at end of file diff --git a/Tests/SmartMigrations.Test/MigrationAttributeNewSchemaTests.cs b/Tests/SmartMigrations.Test/MigrationAttributeNewSchemaTests.cs new file mode 100644 index 0000000..8ab4824 --- /dev/null +++ b/Tests/SmartMigrations.Test/MigrationAttributeNewSchemaTests.cs @@ -0,0 +1,74 @@ +namespace SmartMigrations.Test; + +using SmartMigrations; +using Xunit; + +public class MigrationAttributeNewSchemaTests +{ + #region Constructor: MigrationAttribute(int to, bool shouldAvoid = false) + + [Theory] + [InlineData(0, false)] + [InlineData(0, true)] + [InlineData(1, false)] + [InlineData(1, true)] + [InlineData(30, null)] + [InlineData(30, true)] + [InlineData(int.MaxValue, false)] + [InlineData(int.MaxValue, true)] + public void Constructor_DefaultSchema_InitialSetup_Success(int to, bool? shouldAvoid) + { + var attribute = shouldAvoid != null + ? new MigrationAttribute(to, shouldAvoid.Value) + : new MigrationAttribute(to); + Assert.Null(attribute.FromSchema); + Assert.Empty(attribute.FromVersions); + Assert.False(attribute.IsRange); + Assert.Null(attribute.ToSchema); + Assert.Equal(to, attribute.ToVersion); + if (shouldAvoid.HasValue) Assert.Equal(shouldAvoid.Value, attribute.ShouldAvoid); + else Assert.False(attribute.ShouldAvoid); + } + + #endregion + + #region Constructor: MigrationAttribute(string? schema, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, 0, false, null)] + [InlineData(null, 1, true, null)] + [InlineData("free", 0, false, "free")] + [InlineData("free", 135, true, "free")] + [InlineData("paid", 100, false, "paid")] + [InlineData("paid", -100, true, "paid")] + [InlineData("enterprise", int.MaxValue, false, "enterprise")] + [InlineData("enterprise", int.MaxValue, true, "enterprise")] + [InlineData("custom_schema", int.MinValue, false, "custom_schema")] + [InlineData("custom_schema", int.MinValue, true, "custom_schema")] + [InlineData(" a schema ", -315498, false, "a schema")] + [InlineData("\n\t\nsomeThing123 \n", 69420, true, "someThing123")] + public void Constructor_SpecificSchema_InitialSetup_Success(string? schema, int to, bool shouldAvoid, string? expectedSchema) + { + var attribute = new MigrationAttribute(schema, to, shouldAvoid); + Assert.Equal(expectedSchema, attribute.FromSchema); + Assert.Empty(attribute.FromVersions); + Assert.False(attribute.IsRange); + Assert.Equal(expectedSchema, attribute.ToSchema); + Assert.Equal(to, attribute.ToVersion); + Assert.Equal(shouldAvoid, attribute.ShouldAvoid); + } + + [Theory] + [InlineData("", 1, false)] + [InlineData(" ", 1, true)] + [InlineData("\t", 1, false)] + [InlineData("\n", 1, true)] + [InlineData("\t \n", 1, false)] + [InlineData("\n\n\n \n", 1, true)] + public void Constructor_SpecificSchema_InitialSetup_InvalidSchema_ThrowsException(string schema, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(schema, to, shouldAvoid)); + } + + #endregion +} diff --git a/Tests/SmartMigrations.Test/MigrationAttributeRangeFromTests.cs b/Tests/SmartMigrations.Test/MigrationAttributeRangeFromTests.cs new file mode 100644 index 0000000..0c18096 --- /dev/null +++ b/Tests/SmartMigrations.Test/MigrationAttributeRangeFromTests.cs @@ -0,0 +1,174 @@ +namespace SmartMigrations.Test; + +using SmartMigrations; +using Xunit; + +public class MigrationAttributeRangeFromTests +{ + #region Constructor: MigrationAttribute(int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(1, 3, 10, false, new int[] { 1, 3 }, 10, true, false)] + [InlineData(5, 5, 8, true, new int[] { 5, 5 }, 8, true, true)] + [InlineData(0, 2, 5, false, new int[] { 0, 2 }, 5, true, false)] + [InlineData(-10, 15, 20, true, new int[] { -10, 15 }, 20, true, true)] + [InlineData(0, 0, 1, false, new int[] { 0, 0 }, 1, true, false)] + [InlineData(100, 200, 300, false, new int[] { 100, 200 }, 300, true, false)] + public void Constructor_DefaultSchema_RangeFrom_Success(int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(fromRangeStart, fromRangeEnd, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Null(attribute.FromSchema); + Assert.Null(attribute.ToSchema); + } + + [Theory] + [InlineData(5, 3, 10, false)] + [InlineData(10, 5, 15, true)] + [InlineData(-10, -55, 15, true)] + [InlineData(100, 50, 200, false)] + public void Constructor_DefaultSchema_RangeFrom_InvalidRange_ThrowsException(int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromRangeStart, fromRangeEnd, to, shouldAvoid)); + } + + [Theory] + [InlineData(5, 10, 7, false)] + [InlineData(1, 5, 3, true)] + [InlineData(-10, 20, 15, false)] + [InlineData(5, 10, 5, true)] + [InlineData(5, 10, 10, false)] + public void Constructor_DefaultSchema_RangeFrom_ToInRange_ThrowsException(int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromRangeStart, fromRangeEnd, to, shouldAvoid)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? schema, int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, 1, 5, 10, false, new int[] { 1, 5 }, 10, true, false)] + [InlineData(null, 1, 5, 10, true, new int[] { 1, 5 }, 10, true, true)] + [InlineData("free", 0, 10, 20, false, new int[] { 0, 10 }, 20, true, false)] + [InlineData("free", 0, 10, 20, true, new int[] { 0, 10 }, 20, true, true)] + [InlineData("paid", -10, 10, 20, false, new int[] { -10, 10 }, 20, true, false)] + [InlineData("paid", -10, 10, 20, true, new int[] { -10, 10 }, 20, true, true)] + [InlineData("enterprise", -100, -50, -25, false, new int[] { -100, -50 }, -25, true, false)] + [InlineData("enterprise", -100, -50, -25, true, new int[] { -100, -50 }, -25, true, true)] + public void Constructor_SpecificSchema_RangeFrom_Success(string? schema, int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(schema, fromRangeStart, fromRangeEnd, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(schema, attribute.FromSchema); + Assert.Equal(schema, attribute.ToSchema); + } + + [Theory] + [InlineData("", 1, 5, 10, false)] + [InlineData(" ", 1, 5, 10, false)] + [InlineData("\t", 1, 5, 10, false)] + [InlineData("\n", 1, 5, 10, false)] + public void Constructor_SpecificSchema_RangeFrom_InvalidSchema_ThrowsException(string schema, int fromRangeStart, int fromRangeEnd, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(schema, fromRangeStart, fromRangeEnd, to, shouldAvoid)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? fromSchema, int fromRangeStart, int fromRangeEnd, string? toSchema, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, 1, 5, null, 10, false, new int[] { 1, 5 }, 10, true, false)] + [InlineData(null, 1, 5, null, 10, true, new int[] { 1, 5 }, 10, true, true)] + [InlineData("free", 0, 10, "free", 20, false, new int[] { 0, 10 }, 20, true, false)] + [InlineData("free", 0, 10, "free", 20, true, new int[] { 0, 10 }, 20, true, true)] + [InlineData("free", 5, 15, "paid", 1, false, new int[] { 5, 15 }, 1, true, false)] + [InlineData("free", 5, 15, "paid", 1, true, new int[] { 5, 15 }, 1, true, true)] + [InlineData("paid", 10, 20, "enterprise", 1, false, new int[] { 10, 20 }, 1, true, false)] + [InlineData("paid", 10, 20, "enterprise", 1, true, new int[] { 10, 20 }, 1, true, true)] + [InlineData(null, 5, 10, "premium", 1, false, new int[] { 5, 10 }, 1, true, false)] + [InlineData("basic", 5, 10, null, 15, false, new int[] { 5, 10 }, 15, true, false)] + public void Constructor_CrossSchema_RangeFrom_Success(string? fromSchema, int fromRangeStart, int fromRangeEnd, string? toSchema, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(fromSchema, fromRangeStart, fromRangeEnd, toSchema, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(fromSchema, attribute.FromSchema); + Assert.Equal(toSchema, attribute.ToSchema); + } + + [Theory] + [InlineData("", 1, 5, "paid", 10, false)] + [InlineData(" ", 1, 5, "paid", 10, false)] + [InlineData("free", 1, 5, "", 10, false)] + [InlineData("free", 1, 5, " ", 10, false)] + [InlineData("\t", 1, 5, "paid", 10, false)] + [InlineData("free", 1, 5, "\n", 10, false)] + public void Constructor_CrossSchema_RangeFrom_InvalidSchema_ThrowsException(string fromSchema, int fromRangeStart, int fromRangeEnd, string toSchema, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromSchema, fromRangeStart, fromRangeEnd, toSchema, to, shouldAvoid)); + } + + #endregion + + #region Edge Cases for Range From Migrations + + [Fact] + public void Constructor_RangeFrom_IsRangeTrue() + { + var attribute = new MigrationAttribute(1, 10, 20); + Assert.True(attribute.IsRange); + } + + [Fact] + public void Constructor_RangeFrom_FromVersionsContainsTwoElements() + { + var attribute = new MigrationAttribute(5, 15, 20); + Assert.Equal(2, attribute.FromVersions.Length); + Assert.Equal(5, attribute.FromVersions[0]); + Assert.Equal(15, attribute.FromVersions[1]); + } + + [Fact] + public void Constructor_RangeFrom_SingleValueRange() + { + var attribute = new MigrationAttribute(5, 5, 10); + Assert.Equal(new int[] { 5, 5 }, attribute.FromVersions); + Assert.True(attribute.IsRange); + } + + [Fact] + public void Constructor_RangeFrom_LargeRange() + { + var attribute = new MigrationAttribute(1, 10000, 10001); + Assert.Equal(new int[] { 1, 10000 }, attribute.FromVersions); + Assert.Equal(10001, attribute.ToVersion); + } + + [Fact] + public void Constructor_RangeFrom_NegativeRange() + { + var attribute = new MigrationAttribute(-1000, -500, -250); + Assert.Equal(new int[] { -1000, -500 }, attribute.FromVersions); + Assert.Equal(-250, attribute.ToVersion); + } + + [Fact] + public void Constructor_RangeFrom_CrossingZeroRange() + { + var attribute = new MigrationAttribute(-100, 100, 200); + Assert.Equal(new int[] { -100, 100 }, attribute.FromVersions); + Assert.Equal(200, attribute.ToVersion); + } + + #endregion +} \ No newline at end of file diff --git a/Tests/SmartMigrations.Test/MigrationAttributeSingleFromTests.cs b/Tests/SmartMigrations.Test/MigrationAttributeSingleFromTests.cs new file mode 100644 index 0000000..2b1a77d --- /dev/null +++ b/Tests/SmartMigrations.Test/MigrationAttributeSingleFromTests.cs @@ -0,0 +1,133 @@ +namespace SmartMigrations.Test; + +using SmartMigrations; +using Xunit; + +public class MigrationAttributeSingleFromTests +{ + #region Constructor: MigrationAttribute(int from, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(0, 1, false, new[] { 0 })] + [InlineData(5, 1, true, new[] { 5 })] + [InlineData(8, 31, null, new[] { 8 })] + [InlineData(8, -105631, true, new[] { 8 })] + [InlineData(168, -50, false, new[] { 168 })] + [InlineData(-168, 50, true, new[] { -168 })] + [InlineData(0, int.MaxValue, false, new[] { 0 })] + [InlineData(int.MaxValue, int.MinValue, true, new[] { int.MaxValue })] + public void Constructor_DefaultSchema_SingleFrom_Success(int from, int to, bool? shouldAvoid, int[] expectedFromVersions) + { + var attribute = shouldAvoid.HasValue + ? new MigrationAttribute(from, to, shouldAvoid.Value) + : new MigrationAttribute(from, to); + Assert.Null(attribute.FromSchema); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.False(attribute.IsRange); + Assert.Null(attribute.ToSchema); + Assert.Equal(to, attribute.ToVersion); + if (shouldAvoid.HasValue) Assert.Equal(shouldAvoid.Value, attribute.ShouldAvoid); + else Assert.False(attribute.ShouldAvoid); + } + + [Theory] + [InlineData(0, 0, false)] + [InlineData(5, 5, true)] + [InlineData(-42, -42, true)] + [InlineData(100, 100, false)] + [InlineData(int.MaxValue, int.MaxValue, true)] + [InlineData(int.MinValue, int.MinValue, true)] + public void Constructor_DefaultSchema_SingleFrom_SameFromTo_ThrowsException(int from, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(from, to, shouldAvoid)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? schema, int from, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, 0, 1, false, new[] { 0 }, null)] + [InlineData(null, 0, -1, true, new[] { 0 }, null)] + [InlineData("free", 1, 2, false, new[] { 1 }, "free")] + [InlineData("fr ee", -6, 2, true, new[] { -6 }, "fr ee")] + [InlineData("paid ", 10, 20, false, new[] { 10 }, "paid")] + [InlineData(" \npaid", int.MaxValue, 20, true, new[] { int.MaxValue }, "paid")] + [InlineData("enterprise", 100, 50, false, new[] { 100 }, "enterprise")] + [InlineData("\n enterprise \t\t", int.MinValue, int.MinValue + 1, true, new[] { int.MinValue }, "enterprise")] + [InlineData(" 123 How Are You?\n\r", 100, -50, true, new[] { 100 }, "123 How Are You?")] + public void Constructor_SpecificSchema_SingleFrom_Success(string? schema, int from, int to, bool shouldAvoid, int[] expectedFromVersions, string? expectedSchema) + { + var attribute = new MigrationAttribute(schema, from, to, shouldAvoid); + Assert.Equal(expectedSchema, attribute.FromSchema); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.False(attribute.IsRange); + Assert.Equal(expectedSchema, attribute.ToSchema); + Assert.Equal(to, attribute.ToVersion); + Assert.Equal(shouldAvoid, attribute.ShouldAvoid); + } + + [Theory] + [InlineData("", 1, 2)] + [InlineData(" ", 1, 2)] + [InlineData("\t", 1, 2)] + [InlineData("\n", 1, 2)] + public void Constructor_SpecificSchema_SingleFrom_InvalidSchema_ThrowsException(string schema, int from, int to) + { + Assert.Throws(() => new MigrationAttribute(schema, from, to)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? fromSchema, int from, string? toSchema, int to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, 1, null, 2, true, new[] { 1 }, null, null)] + [InlineData(null, -11, null, 2, false, new[] { -11 }, null, null)] + [InlineData("free", 1, "free", 200, null, new[] { 1 }, "free", "free")] + [InlineData("free", -101, "free", -200, true, new[] { -101 }, "free", "free")] + [InlineData("free", 10, "paid", 1, false, new[] { 10 }, "free", "paid")] + [InlineData("free ", 10, " paid", 1, null, new[] { 10 }, "free", "paid")] + [InlineData("paid\n", 5, "enterprise", 1, true, new[] { 5 }, "paid", "enterprise")] + [InlineData("paid", 5, "enterprise", 1, false, new[] { 5 }, "paid", "enterprise")] + [InlineData(null, 5, "premium\n\n", 1, null, new[] { 5 }, null, "premium")] + [InlineData("basic", 5, null, 10, true, new[] { 5 }, "basic", null)] + public void Constructor_CrossSchema_SingleFrom_Success(string? fromSchema, int from, string? toSchema, int to, bool? shouldAvoid, int[] expectedFromVersions, string? expectedFromSchema, string? expectedToSchema) + { + var attribute = shouldAvoid.HasValue + ? new MigrationAttribute(fromSchema, from, toSchema, to, shouldAvoid.Value) + : new MigrationAttribute(fromSchema, from, toSchema, to); + Assert.Equal(expectedFromSchema, attribute.FromSchema); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.False(attribute.IsRange); + Assert.Equal(expectedToSchema, attribute.ToSchema); + Assert.Equal(to, attribute.ToVersion); + if (shouldAvoid.HasValue) Assert.Equal(shouldAvoid.Value, attribute.ShouldAvoid); + else Assert.False(attribute.ShouldAvoid); + } + + [Theory] + [InlineData("", 1, "paid", 2)] + [InlineData(" ", 1, "paid", 2)] + [InlineData("free", 1, "", 2)] + [InlineData("free", 1, " ", 2)] + [InlineData("\t", 1, "paid", 2)] + [InlineData("free", 1, "\n", 2)] + [InlineData(" ", 1, "\n", 2)] + public void Constructor_CrossSchema_SingleFrom_InvalidSchema_ThrowsException(string fromSchema, int from, string toSchema, int to) + { + Assert.Throws(() => new MigrationAttribute(fromSchema, from, toSchema, to)); + } + + [Theory] + [InlineData("free", 5, "paid", 5, false)] + [InlineData("free", 5, "paid ", 5, true)] + [InlineData(null, 10, null, 10, false)] + [InlineData("\n\nenterprise", -1, " enterprise \t\r\n ", -1, true)] + public void Constructor_CrossSchema_SingleFrom_SameFromTo_ThrowsException(string? fromSchema, int from, string? toSchema, int to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromSchema, from, toSchema, to, shouldAvoid)); + } + + #endregion +} diff --git a/Tests/SmartMigrations.Test/MigrationAttributeStringTests.cs b/Tests/SmartMigrations.Test/MigrationAttributeStringTests.cs new file mode 100644 index 0000000..a94fe85 --- /dev/null +++ b/Tests/SmartMigrations.Test/MigrationAttributeStringTests.cs @@ -0,0 +1,241 @@ +namespace SmartMigrations.Test; + +using SmartMigrations; +using Xunit; + +public class MigrationAttributeStringTests +{ + #region Constructor: MigrationAttribute(string? from, string to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, "5", false, new int[0], 5, false, false)] + [InlineData(null, "-15", false, new int[0], -15, false, false)] + [InlineData("3", "10", true, new int[] { 3 }, 10, false, true)] + [InlineData("3", "-11", false, new int[] { 3 }, -11, false, false)] + [InlineData("-9", "-8", true, new int[] { -9 }, -8, false, true)] + [InlineData("-9", "0", true, new int[] { -9 }, 0, false, true)] + [InlineData("-46", "158", true, new int[] { -46 }, 158, false, true)] + [InlineData("1,3,5", "8", false, new int[] { 1, 3, 5 }, 8, false, false)] + [InlineData("2..5", "10", true, new int[] { 2, 5 }, 10, true, true)] + [InlineData("0", "1", false, new int[] { 0 }, 1, false, false)] + [InlineData(" 1 , 2 , 3 ", " 10 ", false, new int[] { 1, 2, 3 }, 10, false, false)] + [InlineData("1,2,2, 3,1", "15", true, new int[] { 1, 2, 3 }, 15, false, true)] + [InlineData("-1, 2,16, -94,-1", "15", true, new int[] { -1, 2, 16, -94 }, 15, false, true)] + [InlineData("0..0", "5", false, new int[] { 0, 0 }, 5, true, false)] + [InlineData("-670..12", "50", true, new int[] { -670, 12 }, 50, true, true)] + [InlineData("-21..-2", "-1", true, new int[] { -21, -2 }, -1, true, true)] + [InlineData("-10000..-9800", "-1358", false, new int[] { -10000, -9800 }, -1358, true, false)] + [InlineData("100,50,75", "200", true, new int[] { 100, 50, 75 }, 200, false, true)] + public void Constructor_DefaultSchema_StringFrom_Success(string? from, string to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(from, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Null(attribute.FromSchema); + Assert.Null(attribute.ToSchema); + } + + [Theory] + [InlineData("5", "")] + [InlineData("5", " ")] + [InlineData("", "10")] + [InlineData(" ", "10")] + [InlineData("abc", "10")] + [InlineData("5", "abc")] + [InlineData("..", "10")] + [InlineData("..8", "10")] + [InlineData("8..", "10")] + [InlineData("1.5", "10")] + [InlineData("8..10..", "30")] + [InlineData("8..10..12", "30")] + [InlineData("..8..10", "30")] + [InlineData("3...5", "10")] + [InlineData("5..3", "10")] + [InlineData("-12..-17", "10")] + [InlineData("1..-117", "10")] + [InlineData(",", "10")] + [InlineData("1,2,-3", "2")] + [InlineData("5", "5")] + [InlineData("3..7", "5")] + [InlineData("10..20", "15")] + [InlineData("-30..20", "-25")] + [InlineData("-30..-20", "-21")] + [InlineData("5..10", "5")] + [InlineData("-5..10", "-5")] + [InlineData("5..10", "10")] + [InlineData("-25..-10", "-10")] + public void Constructor_DefaultSchema_StringFrom_InvalidFormat_ThrowsException(string from, string to) + { + Assert.ThrowsAny(() => new MigrationAttribute(from, to, false)); + } + + [Fact] + public void Constructor_DefaultSchema_StringFrom_NullTo_ThrowsException() + { + Assert.Throws(() => new MigrationAttribute("5", null!, false)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? schema, string? from, string to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, null, "10", false, new int[0], 10, false, false)] + [InlineData(null, null, "10", true, new int[0], 10, false, true)] + [InlineData("free", "5", "10", false, new int[] { 5 }, 10, false, false)] + [InlineData("free", "5", "10", true, new int[] { 5 }, 10, false, true)] + [InlineData("paid", "1,2,3", "10", false, new int[] { 1, 2, 3 }, 10, false, false)] + [InlineData("paid", "1,2,3", "10", true, new int[] { 1, 2, 3 }, 10, false, true)] + [InlineData("enterprise", "5..10", "15", false, new int[] { 5, 10 }, 15, true, false)] + [InlineData("enterprise", "5..10", "15", true, new int[] { 5, 10 }, 15, true, true)] + public void Constructor_SpecificSchema_StringFrom_Success(string? schema, string? from, string to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(schema, from, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(schema, attribute.FromSchema); + Assert.Equal(schema, attribute.ToSchema); + } + + [Theory] + [InlineData("", "5", "10", false)] + [InlineData(" ", "5", "10", false)] + [InlineData("\t", "5", "10", false)] + [InlineData("\n", "5", "10", false)] + public void Constructor_SpecificSchema_StringFrom_InvalidSchema_ThrowsException(string schema, string from, string to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(schema, from, to, shouldAvoid)); + } + + #endregion + + #region Constructor: MigrationAttribute(string? fromSchema, string? from, string? toSchema, string to, bool shouldAvoid = false) + + [Theory] + [InlineData(null, null, null, "10", false, new int[0], 10, false, false)] + [InlineData(null, null, null, "10", true, new int[0], 10, false, true)] + [InlineData("free", "5", "free", "10", false, new int[] { 5 }, 10, false, false)] + [InlineData("free", "5", "free", "10", true, new int[] { 5 }, 10, false, true)] + [InlineData("free", "10,15,20", "paid", "1", false, new int[] { 10, 15, 20 }, 1, false, false)] + [InlineData("free", "10,15,20", "paid", "1", true, new int[] { 10, 15, 20 }, 1, false, true)] + [InlineData("paid", "5..15", "enterprise", "1", false, new int[] { 5, 15 }, 1, true, false)] + [InlineData("paid", "5..15", "enterprise", "1", true, new int[] { 5, 15 }, 1, true, true)] + [InlineData(null, "5", "premium", "1", false, new int[] { 5 }, 1, false, false)] + [InlineData("basic", "5", null, "10", false, new int[] { 5 }, 10, false, false)] + public void Constructor_CrossSchema_StringFrom_Success(string? fromSchema, string? from, string? toSchema, string to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) + { + var attribute = new MigrationAttribute(fromSchema, from, toSchema, to, shouldAvoid); + Assert.Equal(expectedFromVersions, attribute.FromVersions); + Assert.Equal(expectedTo, attribute.ToVersion); + Assert.Equal(expectedIsRange, attribute.IsRange); + Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); + Assert.Equal(fromSchema, attribute.FromSchema); + Assert.Equal(toSchema, attribute.ToSchema); + } + + [Theory] + [InlineData("", "5", "paid", "10", false)] + [InlineData(" ", "5", "paid", "10", false)] + [InlineData("free", "5", "", "10", false)] + [InlineData("free", "5", " ", "10", false)] + [InlineData("\t", "5", "paid", "10", false)] + [InlineData("free", "5", "\n", "10", false)] + public void Constructor_CrossSchema_StringFrom_InvalidSchema_ThrowsException(string fromSchema, string from, string toSchema, string to, bool shouldAvoid) + { + Assert.Throws(() => new MigrationAttribute(fromSchema, from, toSchema, to, shouldAvoid)); + } + + #endregion + + #region Edge Cases for String Constructors + + [Fact] + public void Constructor_String_SingleVersionParsing() + { + var attribute = new MigrationAttribute("5", "10"); + Assert.Equal(new int[] { 5 }, attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + Assert.False(attribute.IsRange); + } + + [Fact] + public void Constructor_String_ListVersionParsing() + { + var attribute = new MigrationAttribute("1,2,3", "10"); + Assert.Equal(new int[] { 1, 2, 3 }, attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + Assert.False(attribute.IsRange); + } + + [Fact] + public void Constructor_String_RangeVersionParsing() + { + var attribute = new MigrationAttribute("5..10", "15"); + Assert.Equal(new int[] { 5, 10 }, attribute.FromVersions); + Assert.Equal(15, attribute.ToVersion); + Assert.True(attribute.IsRange); + } + + [Fact] + public void Constructor_String_WhitespaceHandling() + { + var attribute = new MigrationAttribute(" 5 ", " 10 "); + Assert.Equal(new int[] { 5 }, attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + } + + [Fact] + public void Constructor_String_ListWithWhitespace() + { + var attribute = new MigrationAttribute("1, 2, 3", "10"); + Assert.Equal(new int[] { 1, 2, 3 }, attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + } + + [Fact] + public void Constructor_String_ListDuplicatesRemoved() + { + var attribute = new MigrationAttribute("1,2,2,3,1", "10"); + Assert.Equal(new int[] { 1, 2, 3 }, attribute.FromVersions); + Assert.Equal(10, attribute.ToVersion); + } + + [Fact] + public void Constructor_String_NegativeVersions() + { + var attribute = new MigrationAttribute("-5", "-1"); + Assert.Equal(new int[] { -5 }, attribute.FromVersions); + Assert.Equal(-1, attribute.ToVersion); + } + + [Fact] + public void Constructor_String_NegativeList() + { + var attribute = new MigrationAttribute("-5,-3,-1", "0"); + Assert.Equal(new int[] { -5, -3, -1 }, attribute.FromVersions); + Assert.Equal(0, attribute.ToVersion); + } + + [Fact] + public void Constructor_String_NegativeRange() + { + var attribute = new MigrationAttribute("-10..-5", "0"); + Assert.Equal(new int[] { -10, -5 }, attribute.FromVersions); + Assert.Equal(0, attribute.ToVersion); + Assert.True(attribute.IsRange); + } + + [Fact] + public void Constructor_String_LargeNumbers() + { + var attribute = new MigrationAttribute("1000000", "2000000"); + Assert.Equal(new int[] { 1000000 }, attribute.FromVersions); + Assert.Equal(2000000, attribute.ToVersion); + } + + #endregion +} \ No newline at end of file diff --git a/Tests/SmartMigrations.Test/MigrationAttributeTest.cs b/Tests/SmartMigrations.Test/MigrationAttributeTest.cs deleted file mode 100644 index 2130068..0000000 --- a/Tests/SmartMigrations.Test/MigrationAttributeTest.cs +++ /dev/null @@ -1,297 +0,0 @@ -namespace SmartMigrations.Test; - -using SmartMigrations; -using Xunit; - -public class MigrationAttributeTest -{ - #region Constructor: MigrationAttribute(int to, bool shouldAvoid) - - [Theory] - [InlineData(0, false, new int[0], 0, false, false)] - [InlineData(0, true, new int[0], 0, false, true)] - [InlineData(1, false, new int[0], 1, false, false)] - [InlineData(1, true, new int[0], 1, false, true)] - [InlineData(30, false, new int[0], 30, false, false)] - [InlineData(30, true, new int[0], 30, false, true)] - [InlineData(int.MaxValue, false, new int[0], int.MaxValue, false, false)] - [InlineData(int.MaxValue, true, new int[0], int.MaxValue, false, true)] - public void Constructor_ToOnly_Success(int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) - { - var attribute = new MigrationAttribute(to, shouldAvoid); - Assert.Equal(expectedFromVersions, attribute.FromVersions); - Assert.Equal(expectedTo, attribute.ToVersion); - Assert.Equal(expectedIsRange, attribute.IsRange); - Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); - } - - #endregion - - #region Constructor: MigrationAttribute(int from, int to, bool shouldAvoid) - - [Theory] - [InlineData(0, 1, false, new int[] { 0 }, 1, false, false)] - [InlineData(0, 1, true, new int[] { 0 }, 1, false, true)] - [InlineData(8, 31, false, new int[] { 8 }, 31, false, false)] - [InlineData(8, 31, true, new int[] { 8 }, 31, false, true)] - [InlineData(168, 50, false, new int[] { 168 }, 50, false, false)] - [InlineData(168, 50, true, new int[] { 168 }, 50, false, true)] - [InlineData(0, int.MaxValue, false, new int[] { 0 }, int.MaxValue, false, false)] - [InlineData(0, int.MaxValue, true, new int[] { 0 }, int.MaxValue, false, true)] - public void Constructor_SingleFrom_Success(int from, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) - { - var attribute = new MigrationAttribute(from, to, shouldAvoid); - Assert.Equal(expectedFromVersions, attribute.FromVersions); - Assert.Equal(expectedTo, attribute.ToVersion); - Assert.Equal(expectedIsRange, attribute.IsRange); - Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); - } - - [Theory] - [InlineData(0, 0, false)] - [InlineData(5, 5, true)] - [InlineData(-42, -42, true)] - [InlineData(100, 100, false)] - [InlineData(int.MaxValue, int.MaxValue, true)] - public void Constructor_SingleFrom_ThrowsArgumentException(int from, int to, bool shouldAvoid) - { - Assert.Throws(() => new MigrationAttribute(from, to, shouldAvoid)); - } - - #endregion - - #region Constructor: MigrationAttribute(IEnumerable fromList, int to, bool shouldAvoid) - - [Theory] - [InlineData(new int[] { 1 }, 5, false, new int[] { 1 }, 5, false, false)] - [InlineData(new int[] { 1, 3, 5 }, 10, true, new int[] { 1, 3, 5 }, 10, false, true)] - [InlineData(new int[] { 0, 1, 2 }, 3, false, new int[] { 0, 1, 2 }, 3, false, false)] - [InlineData(new int[] { 10, 20, 30, 40 }, 50, true, new int[] { 10, 20, 30, 40 }, 50, false, true)] - [InlineData(new int[] { 100 }, 200, false, new int[] { 100 }, 200, false, false)] - [InlineData(new int[] { 5, 1, 3 }, 10, false, new int[] { 5, 1, 3 }, 10, false, false)] // Preserves order, removes duplicates - [InlineData(new int[] { 5, 1, 3, 1, 5 }, 10, true, new int[] { 5, 1, 3 }, 10, false, true)] // Duplicates removed, order preserved - public void Constructor_FromList_Success(int[] fromList, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) - { - var attribute = new MigrationAttribute(fromList, to, shouldAvoid); - Assert.Equal(expectedFromVersions, attribute.FromVersions); - Assert.Equal(expectedTo, attribute.ToVersion); - Assert.Equal(expectedIsRange, attribute.IsRange); - Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); - } - - [Theory] - [InlineData(new int[] { 1, 5 }, 5, false)] // Contains to version - [InlineData(new int[] { 7, -12 }, 7, false)] // Contains to version - [InlineData(new int[] { 7, -12 }, -12, true)] // Contains to version - [InlineData(new int[] { 10, 20, 30 }, 20, true)] // Contains to version in middle - [InlineData(new int[] { 5 }, 5, false)] // Single item equals to - public void Constructor_FromList_WithToVersionInList_ThrowsArgumentException(int[] fromList, int to, bool shouldAvoid) - { - Assert.Throws(() => new MigrationAttribute(fromList, to, shouldAvoid)); - } - - [Fact] - public void Constructor_FromList_WithNull_ThrowsArgumentNullException() - { - Assert.Throws(() => new MigrationAttribute(null!, 5)); - } - - #endregion - - #region Constructor: MigrationAttribute((int start, int end) fromRange, int to, bool shouldAvoid) - - [Theory] - [InlineData(1, 3, 10, false, new int[] { 1, 3 }, 10, true, false)] // Note: Range stores [start, end] not full sequence - [InlineData(5, 5, 8, true, new int[] { 5, 5 }, 8, true, true)] // Single value range - [InlineData(0, 2, 5, false, new int[] { 0, 2 }, 5, true, false)] - [InlineData(-10, 15, 20, true, new int[] { -10, 15 }, 20, true, true)] - [InlineData(0, 0, 1, false, new int[] { 0, 0 }, 1, true, false)] - [InlineData(100, 200, 300, false, new int[] { 100, 200 }, 300, true, false)] - public void Constructor_Range_Success(int start, int end, int to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) - { - var attribute = new MigrationAttribute(start, end, to, shouldAvoid); - Assert.Equal(expectedFromVersions, attribute.FromVersions); - Assert.Equal(expectedTo, attribute.ToVersion); - Assert.Equal(expectedIsRange, attribute.IsRange); - Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); - } - - [Theory] - [InlineData(5, 3, 10, false)] - [InlineData(10, 5, 15, true)] - [InlineData(-10, -55, 15, true)] - [InlineData(100, 50, 200, false)] - public void Constructor_Range_WithInvalidRange_ThrowsArgumentException(int start, int end, int to, bool shouldAvoid) - { - Assert.Throws(() => new MigrationAttribute(start, end, to, shouldAvoid)); - } - - [Theory] - [InlineData(5, 10, 7, false)] - [InlineData(1, 5, 3, true)] - [InlineData(-10, 20, 15, false)] - [InlineData(5, 10, 5, true)] - [InlineData(5, 10, 10, false)] - public void Constructor_Range_WithToVersionInRange_ThrowsArgumentException(int start, int end, int to, bool shouldAvoid) - { - Assert.Throws(() => new MigrationAttribute(start, end, to, shouldAvoid)); - } - - #endregion - - #region Constructor: MigrationAttribute(string? from, string to, bool shouldAvoid) - - [Theory] - [InlineData(null, "5", false, new int[0], 5, false, false)] - [InlineData(null, "-15", false, new int[0], -15, false, false)] - [InlineData("3", "10", true, new int[] { 3 }, 10, false, true)] - [InlineData("3", "-11", false, new int[] { 3 }, -11, false, false)] - [InlineData("-9", "-8", true, new int[] { -9 }, -8, false, true)] - [InlineData("-9", "0", true, new int[] { -9 }, 0, false, true)] - [InlineData("-46", "158", true, new int[] { -46 }, 158, false, true)] - [InlineData("1,3,5", "8", false, new int[] { 1, 3, 5 }, 8, false, false)] - [InlineData("2..5", "10", true, new int[] { 2, 5 }, 10, true, true)] - [InlineData("0", "1", false, new int[] { 0 }, 1, false, false)] - [InlineData(" 1 , 2 , 3 ", " 10 ", false, new int[] { 1, 2, 3 }, 10, false, false)] - [InlineData("1,2,2, 3,1", "15", true, new int[] { 1, 2, 3 }, 15, false, true)] - [InlineData("-1, 2,16, -94,-1", "15", true, new int[] { -1, 2, 16, -94 }, 15, false, true)] - [InlineData("0..0", "5", false, new int[] { 0, 0 }, 5, true, false)] - [InlineData("-670..12", "50", true, new int[] { -670, 12 }, 50, true, true)] - [InlineData("-21..-2", "-1", true, new int[] { -21, -2 }, -1, true, true)] - [InlineData("-10000..-9800", "-1358", false, new int[] { -10000, -9800 }, -1358, true, false)] - [InlineData("100,50,75", "200", true, new int[] { 100, 50, 75 }, 200, false, true)] - public void Constructor_String_Success(string? from, string to, bool shouldAvoid, int[] expectedFromVersions, int expectedTo, bool expectedIsRange, bool expectedShouldAvoid) - { - var attribute = new MigrationAttribute(from, to, shouldAvoid); - Assert.Equal(expectedFromVersions, attribute.FromVersions); - Assert.Equal(expectedTo, attribute.ToVersion); - Assert.Equal(expectedIsRange, attribute.IsRange); - Assert.Equal(expectedShouldAvoid, attribute.ShouldAvoid); - } - - [Theory] - [InlineData("5", "")] // Empty to - [InlineData("5", " ")] // Whitespace to - [InlineData("", "10")] // Empty from - [InlineData(" ", "10")] // Whitespace from - [InlineData("abc", "10")] // Invalid from format - [InlineData("5", "abc")] // Invalid to format - [InlineData("..", "10")] // Invalid range format - [InlineData("..8", "10")] // Invalid range format - [InlineData("8..", "10")] // Invalid range format - [InlineData("1.5", "10")] // Invalid range format - [InlineData("8..10..", "30")] // Invalid range format - [InlineData("8..10..12", "30")] // Invalid range format - [InlineData("..8..10", "30")] // Invalid range format - [InlineData("3...5", "10")] // Invalid range format - [InlineData("5..3", "10")] // Reversed range - [InlineData("-12..-17", "10")] // Reversed range - [InlineData("1..-117", "10")] // Reversed range - [InlineData(",", "10")] // Empty comma list - [InlineData("1,2,-3", "2")] // From list contains to version - [InlineData("5", "5")] // Single from equals to - [InlineData("3..7", "5")] // To within range - [InlineData("10..20", "15")] // To within range - [InlineData("-30..20", "-25")] // To within range - [InlineData("-30..-20", "-21")] // To within range - [InlineData("5..10", "5")] // To equals range start - [InlineData("-5..10", "-5")] // To equals range start - [InlineData("5..10", "10")] // To equals range end - [InlineData("-25..-10", "-10")] // To equals range end - public void Constructor_String_ThrowsException(string? from, string to) - { - Assert.ThrowsAny(() => new MigrationAttribute(from, to, false)); - } - - #endregion - - #region Multiple Attributes Per Class - - [MigrationAttribute(10)] // Constructor 1: to only - [MigrationAttribute(5, 15)] // Constructor 2: single from/to - [MigrationAttribute(new int[] { 1, 3, 7 }, 20)] // Constructor 3: list from/to - [MigrationAttribute(8, 12, 25, true)] // Constructor 4: range from/to with shouldAvoid - [MigrationAttribute(null, "30")] // Constructor 5: string - null from - [MigrationAttribute("2", "35")] // Constructor 6: string - single from - [MigrationAttribute("4,6,9", "40")] // Constructor 7: string - list from - [MigrationAttribute("13..17", "45")] // Constructor 8: string - range from - [MigrationAttribute(18, 50, true)] // Constructor 9: single from/to with shouldAvoid - [MigrationAttribute(new int[] { 21, 23 }, 55, true)] // Constructor 10: list from/to with shouldAvoid - private class TestMigrationWithMultipleAttributes { } - - [Fact] - public void MultipleAttributes_OnSingleClass_ShouldAllBeAccessible() - { - var attributes = typeof(TestMigrationWithMultipleAttributes) - .GetCustomAttributes(typeof(MigrationAttribute), false) - .Cast() - .ToList(); - - Assert.Equal(10, attributes.Count); - - // Verify each attribute has expected properties - var sortedByTo = attributes.OrderBy(a => a.ToVersion).ToList(); - - // Attribute 1: MigrationAttribute(10) - Assert.Empty(sortedByTo[0].FromVersions); - Assert.Equal(10, sortedByTo[0].ToVersion); - Assert.False(sortedByTo[0].IsRange); - Assert.False(sortedByTo[0].ShouldAvoid); - - // Attribute 2: MigrationAttribute(5, 15) - Assert.Equal(new[] { 5 }, sortedByTo[1].FromVersions); - Assert.Equal(15, sortedByTo[1].ToVersion); - Assert.False(sortedByTo[1].IsRange); - Assert.False(sortedByTo[1].ShouldAvoid); - - // Attribute 3: MigrationAttribute(new int[] { 1, 3, 7 }, 20) - Assert.Equal(new[] { 1, 3, 7 }, sortedByTo[2].FromVersions); - Assert.Equal(20, sortedByTo[2].ToVersion); - Assert.False(sortedByTo[2].IsRange); - Assert.False(sortedByTo[2].ShouldAvoid); - - // Attribute 4: MigrationAttribute((8, 12), 25, true) - Assert.Equal(new[] { 8, 12 }, sortedByTo[3].FromVersions); - Assert.Equal(25, sortedByTo[3].ToVersion); - Assert.True(sortedByTo[3].IsRange); - Assert.True(sortedByTo[3].ShouldAvoid); - - // Attribute 5: MigrationAttribute(null, "30") - Assert.Empty(sortedByTo[4].FromVersions); - Assert.Equal(30, sortedByTo[4].ToVersion); - Assert.False(sortedByTo[4].IsRange); - Assert.False(sortedByTo[4].ShouldAvoid); - - // Attribute 6: MigrationAttribute("2", "35") - Assert.Equal(new[] { 2 }, sortedByTo[5].FromVersions); - Assert.Equal(35, sortedByTo[5].ToVersion); - Assert.False(sortedByTo[5].IsRange); - Assert.False(sortedByTo[5].ShouldAvoid); - - // Attribute 7: MigrationAttribute("4,6,9", "40") - Assert.Equal(new[] { 4, 6, 9 }, sortedByTo[6].FromVersions); - Assert.Equal(40, sortedByTo[6].ToVersion); - Assert.False(sortedByTo[6].IsRange); - Assert.False(sortedByTo[6].ShouldAvoid); - - // Attribute 8: MigrationAttribute("13..17", "45") - Assert.Equal(new[] { 13, 17 }, sortedByTo[7].FromVersions); - Assert.Equal(45, sortedByTo[7].ToVersion); - Assert.True(sortedByTo[7].IsRange); - Assert.False(sortedByTo[7].ShouldAvoid); - - // Attribute 9: MigrationAttribute(18, 50, true) - Assert.Equal(new[] { 18 }, sortedByTo[8].FromVersions); - Assert.Equal(50, sortedByTo[8].ToVersion); - Assert.False(sortedByTo[8].IsRange); - Assert.True(sortedByTo[8].ShouldAvoid); - - // Attribute 10: MigrationAttribute(new int[] { 21, 23 }, 55, true) - Assert.Equal(new[] { 21, 23 }, sortedByTo[9].FromVersions); - Assert.Equal(55, sortedByTo[9].ToVersion); - Assert.False(sortedByTo[9].IsRange); - Assert.True(sortedByTo[9].ShouldAvoid); - } - - #endregion -} diff --git a/Tests/SmartMigrations.Test/SmartMigrations.Test.csproj b/Tests/SmartMigrations.Test/SmartMigrations.Test.csproj index d98741c..e2ddfdb 100644 --- a/Tests/SmartMigrations.Test/SmartMigrations.Test.csproj +++ b/Tests/SmartMigrations.Test/SmartMigrations.Test.csproj @@ -10,4 +10,8 @@ + + + +