using Expecto.CSharp; using Expecto; using Microsoft.FSharp.Collections; using Microsoft.FSharp.Core; namespace BitBadger.Documents.Tests.CSharp; using static Runner; /// /// A test serializer that returns known values /// internal class TestSerializer : IDocumentSerializer { public string Serialize(T it) => "{\"Overridden\":true}"; public T Deserialize(string it) => default!; } /// /// C# Tests for common functionality in BitBadger.Documents /// public static class CommonCSharpTests { /// /// Unit tests /// [Tests] public static readonly Test Unit = TestList("Common.C# Unit", [ TestSequenced( TestList("Configuration", [ TestCase("UseSerializer succeeds", () => { try { Configuration.UseSerializer(new TestSerializer()); var serialized = Configuration.Serializer().Serialize(new SubDocument { Foo = "howdy", Bar = "bye" }); Expect.equal(serialized, "{\"Overridden\":true}", "Specified serializer was not used"); var deserialized = Configuration.Serializer() .Deserialize("{\"Something\":\"here\"}"); Expect.isNull(deserialized, "Specified serializer should have returned null"); } finally { Configuration.UseSerializer(DocumentSerializer.Default); } }), TestCase("Serializer returns configured serializer", () => { Expect.isTrue(ReferenceEquals(DocumentSerializer.Default, Configuration.Serializer()), "Serializer should have been the same"); }), TestCase("UseIdField / IdField succeeds", () => { try { Expect.equal(Configuration.IdField(), "Id", "The default configured ID field was incorrect"); Configuration.UseIdField("id"); Expect.equal(Configuration.IdField(), "id", "UseIdField did not set the ID field"); } finally { Configuration.UseIdField("Id"); } }) ])), TestList("Op", [ TestCase("EQ succeeds", () => { Expect.equal(Op.EQ.ToString(), "=", "The equals operator was not correct"); }), TestCase("GT succeeds", () => { Expect.equal(Op.GT.ToString(), ">", "The greater than operator was not correct"); }), TestCase("GE succeeds", () => { Expect.equal(Op.GE.ToString(), ">=", "The greater than or equal to operator was not correct"); }), TestCase("LT succeeds", () => { Expect.equal(Op.LT.ToString(), "<", "The less than operator was not correct"); }), TestCase("LE succeeds", () => { Expect.equal(Op.LE.ToString(), "<=", "The less than or equal to operator was not correct"); }), TestCase("NE succeeds", () => { Expect.equal(Op.NE.ToString(), "<>", "The not equal to operator was not correct"); }), TestCase("BT succeeds", () => { Expect.equal(Op.BT.ToString(), "BETWEEN", "The \"between\" operator was not correct"); }), TestCase("EX succeeds", () => { Expect.equal(Op.EX.ToString(), "IS NOT NULL", "The \"exists\" operator was not correct"); }), TestCase("NEX succeeds", () => { Expect.equal(Op.NEX.ToString(), "IS NULL", "The \"not exists\" operator was not correct"); }) ]), TestList("Field", [ TestCase("EQ succeeds", () => { var field = Field.EQ("Test", 14); Expect.equal(field.Name, "Test", "Field name incorrect"); Expect.equal(field.Op, Op.EQ, "Operator incorrect"); Expect.equal(field.Value, 14, "Value incorrect"); }), TestCase("GT succeeds", () => { var field = Field.GT("Great", "night"); Expect.equal(field.Name, "Great", "Field name incorrect"); Expect.equal(field.Op, Op.GT, "Operator incorrect"); Expect.equal(field.Value, "night", "Value incorrect"); }), TestCase("GE succeeds", () => { var field = Field.GE("Nice", 88L); Expect.equal(field.Name, "Nice", "Field name incorrect"); Expect.equal(field.Op, Op.GE, "Operator incorrect"); Expect.equal(field.Value, 88L, "Value incorrect"); }), TestCase("LT succeeds", () => { var field = Field.LT("Lesser", "seven"); Expect.equal(field.Name, "Lesser", "Field name incorrect"); Expect.equal(field.Op, Op.LT, "Operator incorrect"); Expect.equal(field.Value, "seven", "Value incorrect"); }), TestCase("LE succeeds", () => { var field = Field.LE("Nobody", "KNOWS"); Expect.equal(field.Name, "Nobody", "Field name incorrect"); Expect.equal(field.Op, Op.LE, "Operator incorrect"); Expect.equal(field.Value, "KNOWS", "Value incorrect"); }), TestCase("NE succeeds", () => { var field = Field.NE("Park", "here"); Expect.equal(field.Name, "Park", "Field name incorrect"); Expect.equal(field.Op, Op.NE, "Operator incorrect"); Expect.equal(field.Value, "here", "Value incorrect"); }), TestCase("BT succeeds", () => { var field = Field.BT("Age", 18, 49); Expect.equal(field.Name, "Age", "Field name incorrect"); Expect.equal(field.Op, Op.BT, "Operator incorrect"); Expect.equal(((FSharpList)field.Value).ToArray(), [18, 49], "Value incorrect"); }), TestCase("EX succeeds", () => { var field = Field.EX("Groovy"); Expect.equal(field.Name, "Groovy", "Field name incorrect"); Expect.equal(field.Op, Op.EX, "Operator incorrect"); }), TestCase("NEX succeeds", () => { var field = Field.NEX("Rad"); Expect.equal(field.Name, "Rad", "Field name incorrect"); Expect.equal(field.Op, Op.NEX, "Operator incorrect"); }), TestList("NameToPath", [ TestCase("succeeds for PostgreSQL and a simple name", () => { Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.PostgreSQL), "Path not constructed correctly"); }), TestCase("succeeds for SQLite and a simple name", () => { Expect.equal("data->>'Simple'", Field.NameToPath("Simple", Dialect.SQLite), "Path not constructed correctly"); }), TestCase("succeeds for PostgreSQL and a nested name", () => { Expect.equal("data#>>'{A,Long,Path,to,the,Property}'", Field.NameToPath("A.Long.Path.to.the.Property", Dialect.PostgreSQL), "Path not constructed correctly"); }), TestCase("succeeds for SQLite and a nested name", () => { Expect.equal("data->>'A'->>'Long'->>'Path'->>'to'->>'the'->>'Property'", Field.NameToPath("A.Long.Path.to.the.Property", Dialect.SQLite), "Path not constructed correctly"); }) ]), TestCase("WithParameterName succeeds", () => { var field = Field.EQ("Bob", "Tom").WithParameterName("@name"); Expect.isSome(field.ParameterName, "The parameter name should have been filled"); Expect.equal("@name", field.ParameterName.Value, "The parameter name is incorrect"); }), TestCase("WithQualifier succeeds", () => { var field = Field.EQ("Bill", "Matt").WithQualifier("joe"); Expect.isSome(field.Qualifier, "The table qualifier should have been filled"); Expect.equal("joe", field.Qualifier.Value, "The table qualifier is incorrect"); }), TestList("Path", [ TestCase("succeeds for a PostgreSQL single field with no qualifier", () => { var field = Field.GE("SomethingCool", 18); Expect.equal("data->>'SomethingCool'", field.Path(Dialect.PostgreSQL), "The PostgreSQL path is incorrect"); }), TestCase("succeeds for a PostgreSQL single field with a qualifier", () => { var field = Field.LT("SomethingElse", 9).WithQualifier("this"); Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.PostgreSQL), "The PostgreSQL path is incorrect"); }), TestCase("succeeds for a PostgreSQL nested field with no qualifier", () => { var field = Field.EQ("My.Nested.Field", "howdy"); Expect.equal("data#>>'{My,Nested,Field}'", field.Path(Dialect.PostgreSQL), "The PostgreSQL path is incorrect"); }), TestCase("succeeds for a PostgreSQL nested field with a qualifier", () => { var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); Expect.equal("bird.data#>>'{Nest,Away}'", field.Path(Dialect.PostgreSQL), "The PostgreSQL path is incorrect"); }), TestCase("succeeds for a SQLite single field with no qualifier", () => { var field = Field.GE("SomethingCool", 18); Expect.equal("data->>'SomethingCool'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); }), TestCase("succeeds for a SQLite single field with a qualifier", () => { var field = Field.LT("SomethingElse", 9).WithQualifier("this"); Expect.equal("this.data->>'SomethingElse'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); }), TestCase("succeeds for a SQLite nested field with no qualifier", () => { var field = Field.EQ("My.Nested.Field", "howdy"); Expect.equal("data->>'My'->>'Nested'->>'Field'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); }), TestCase("succeeds for a SQLite nested field with a qualifier", () => { var field = Field.EQ("Nest.Away", "doc").WithQualifier("bird"); Expect.equal("bird.data->>'Nest'->>'Away'", field.Path(Dialect.SQLite), "The SQLite path is incorrect"); }) ]) ]), TestList("FieldMatch.ToString", [ TestCase("succeeds for Any", () => { Expect.equal(FieldMatch.Any.ToString(), "OR", "SQL for Any is incorrect"); }), TestCase("succeeds for All", () => { Expect.equal(FieldMatch.All.ToString(), "AND", "SQL for All is incorrect"); }) ]), TestList("ParameterName.Derive", [ TestCase("succeeds with existing name", () => { ParameterName name = new(); Expect.equal(name.Derive(FSharpOption.Some("@taco")), "@taco", "Name should have been @taco"); Expect.equal(name.Derive(FSharpOption.None), "@field0", "Counter should not have advanced for named field"); }), TestCase("Derive succeeds with non-existent name", () => { ParameterName name = new(); Expect.equal(name.Derive(FSharpOption.None), "@field0", "Anonymous field name should have been returned"); Expect.equal(name.Derive(FSharpOption.None), "@field1", "Counter should have advanced from previous call"); Expect.equal(name.Derive(FSharpOption.None), "@field2", "Counter should have advanced from previous call"); Expect.equal(name.Derive(FSharpOption.None), "@field3", "Counter should have advanced from previous call"); }) ]), TestList("Query", [ TestCase("StatementWhere succeeds", () => { Expect.equal(Query.StatementWhere("q", "r"), "q WHERE r", "Statements not combined correctly"); }), TestList("Definition", [ TestCase("EnsureTableFor succeeds", () => { Expect.equal(Query.Definition.EnsureTableFor("my.table", "JSONB"), "CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)", "CREATE TABLE statement not constructed correctly"); }), TestList("EnsureKey", [ TestCase("succeeds when a schema is present", () => { Expect.equal(Query.Definition.EnsureKey("test.table", Dialect.SQLite), "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'Id'))", "CREATE INDEX for key statement with schema not constructed correctly"); }), TestCase("succeeds when a schema is not present", () => { Expect.equal(Query.Definition.EnsureKey("table", Dialect.PostgreSQL), "CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON table ((data->>'Id'))", "CREATE INDEX for key statement without schema not constructed correctly"); }) ]), TestList("EnsureIndexOn", [ TestCase("succeeds for multiple fields and directions", () => { Expect.equal( Query.Definition.EnsureIndexOn("test.table", "gibberish", ["taco", "guac DESC", "salsa ASC"], Dialect.SQLite), "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table " + "((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)", "CREATE INDEX for multiple field statement incorrect"); }), TestCase("succeeds for nested PostgreSQL field", () => { Expect.equal( Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.PostgreSQL), "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data#>>'{a,b,c}'))", "CREATE INDEX for nested PostgreSQL field incorrect"); }), TestCase("succeeds for nested SQLite field", () => { Expect.equal( Query.Definition.EnsureIndexOn("tbl", "nest", ["a.b.c"], Dialect.SQLite), "CREATE INDEX IF NOT EXISTS idx_tbl_nest ON tbl ((data->>'a'->>'b'->>'c'))", "CREATE INDEX for nested SQLite field incorrect"); }) ]) ]), TestCase("Insert succeeds", () => { Expect.equal(Query.Insert("tbl"), "INSERT INTO tbl VALUES (@data)", "INSERT statement not correct"); }), TestCase("Save succeeds", () => { Expect.equal(Query.Save("tbl"), "INSERT INTO tbl VALUES (@data) ON CONFLICT ((data->>'Id')) DO UPDATE SET data = EXCLUDED.data", "INSERT ON CONFLICT UPDATE statement not correct"); }), TestCase("Count succeeds", () => { Expect.equal(Query.Count("tbl"), "SELECT COUNT(*) AS it FROM tbl", "Count query not correct"); }), TestCase("Exists succeeds", () => { Expect.equal(Query.Exists("tbl", "chicken"), "SELECT EXISTS (SELECT 1 FROM tbl WHERE chicken) AS it", "Exists query not correct"); }), TestCase("Find succeeds", () => { Expect.equal(Query.Find("test.table"), "SELECT data FROM test.table", "Find query not correct"); }), TestCase("Update succeeds", () => { Expect.equal(Query.Update("tbl"), "UPDATE tbl SET data = @data", "Update query not correct"); }), TestCase("Delete succeeds", () => { Expect.equal(Query.Delete("tbl"), "DELETE FROM tbl", "Delete query not correct"); }), TestList("OrderBy", [ TestCase("succeeds for no fields", () => { Expect.equal(Query.OrderBy([], Dialect.PostgreSQL), "", "Order By should have been blank (PostgreSQL)"); Expect.equal(Query.OrderBy([], Dialect.SQLite), "", "Order By should have been blank (SQLite)"); }), TestCase("succeeds for PostgreSQL with one field and no direction", () => { Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.PostgreSQL), " ORDER BY data->>'TestField'", "Order By not constructed correctly"); }), TestCase("succeeds for SQLite with one field and no direction", () => { Expect.equal(Query.OrderBy([Field.Named("TestField")], Dialect.SQLite), " ORDER BY data->>'TestField'", "Order By not constructed correctly"); }), TestCase("succeeds for PostgreSQL with multiple fields and direction", () => { Expect.equal( Query.OrderBy( [ Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), Field.Named("It DESC") ], Dialect.PostgreSQL), " ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC", "Order By not constructed correctly"); }), TestCase("succeeds for SQLite with multiple fields and direction", () => { Expect.equal( Query.OrderBy( [ Field.Named("Nested.Test.Field DESC"), Field.Named("AnotherField"), Field.Named("It DESC") ], Dialect.SQLite), " ORDER BY data->>'Nested'->>'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC", "Order By not constructed correctly"); }), TestCase("succeeds for PostgreSQL numeric fields", () => { Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.PostgreSQL), " ORDER BY (data->>'Test')::numeric", "Order By not constructed correctly for numeric field"); }), TestCase("succeeds for SQLite numeric fields", () => { Expect.equal(Query.OrderBy([Field.Named("n:Test")], Dialect.SQLite), " ORDER BY data->>'Test'", "Order By not constructed correctly for numeric field"); }) ]) ]) ]); }