diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6c46e43 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,105 @@ +name: Test + +on: + push: + branches: + - rewrite + pull_request: + +jobs: + analyze: + name: dart analyze + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Analyze project source + run: dart analyze + + fix: + name: dart fix + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Analyze project source + run: dart fix --dry-run + + format: + name: dart format + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Format + run: dart format --set-exit-if-changed -l120 ./lib + + tests: + needs: [ format, analyze, fix ] + name: Unit tests + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Unit tests + run: dart run test diff --git a/Makefile b/Makefile index 63493f4..7541964 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,15 @@ format: ## Run dart format fix: ## Run dart fix dart fix --apply -fix-project: fix format ## Fix whole project +analyze: ## Run dart analyze + dart analyze + +tests: ## Run unit tests + dart run test run: ## Run dev project - docker compose up --build \ No newline at end of file + docker compose up --build + +fix-project: analyze fix format ## Fix whole project + +check-project: fix-project tests ## Run all checks diff --git a/lib/src/repository/tag.dart b/lib/src/repository/tag.dart index 849d05b..bb3736b 100644 --- a/lib/src/repository/tag.dart +++ b/lib/src/repository/tag.dart @@ -1,8 +1,8 @@ import 'package:injector/injector.dart'; import 'package:logging/logging.dart'; -import 'package:postgres/postgres.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/models/tag.dart'; +import 'package:running_on_dart/src/util/query_builder.dart'; class TagRepository { final _database = Injector.appInstance.get(); @@ -11,17 +11,19 @@ class TagRepository { /// Fetch all existing tags from the database. Future> fetchAllActiveTags() async { - final result = await _database.getConnection().execute(Sql.named(''' - SELECT * FROM tags WHERE enabled = TRUE; - ''')); + final query = SelectQuery.selectAll('tags')..andWhere('enabled = TRUE'); + + final result = await _database.executeQuery(query); return result.map((row) => row.toColumnMap()).map(Tag.fromRow); } Future> fetchActiveTagsByName(String nameQuery) async { - final result = await _database.getConnection().execute(Sql.named(''' - SELECT * FROM tags WHERE enabled = TRUE AND name LIKE @nameQuery; - '''), parameters: {'nameQuery': '%$nameQuery%'}); + final query = SelectQuery.selectAll("tags") + ..andWhere("enabled = TRUE") + ..andWhere("name LIKE @nameQuery"); + + final result = await _database.executeQuery(query, parameters: {'nameQuery': '%$nameQuery%'}); return result.map((row) => row.toColumnMap()).map(Tag.fromRow); } @@ -34,11 +36,11 @@ class TagRepository { return; } - await _database.getConnection().execute(Sql.named(''' - UPDATE tags SET enabled = FALSE WHERE id = @id; - '''), parameters: { - 'id': id, - }); + final query = UpdateQuery("tags") + ..addSet("enabled", "FALSE") + ..andWhere("id = @id"); + + await _database.executeQuery(query, parameters: {'id': id}); } /// Add a tag to the database. @@ -48,21 +50,15 @@ class TagRepository { return; } - final result = await _database.getConnection().execute(Sql.named(''' - INSERT INTO tags ( - name, - content, - enabled, - guild_id, - author_id - ) VALUES ( - @name, - @content, - @enabled, - @guild_id, - @author_id - ) RETURNING id; - '''), parameters: { + final query = InsertQuery("tags") + ..addNamedInsert("name") + ..addNamedInsert("content") + ..addNamedInsert("enabled") + ..addNamedInsert("guild_id") + ..addNamedInsert("author_id") + ..addReturning("id"); + + final result = await _database.executeQuery(query, parameters: { 'name': tag.name, 'content': tag.content, 'enabled': tag.enabled, @@ -79,16 +75,15 @@ class TagRepository { return addTag(tag); } - await _database.getConnection().execute(Sql.named(''' - UPDATE tags SET - name = @name, - content = @content, - enabled = @enabled, - guild_id = @guild_id, - author_id = @author_id - WHERE - id = @id - '''), parameters: { + final query = UpdateQuery("tags") + ..addNamedSet('name') + ..addNamedSet('content') + ..addNamedSet('enabled') + ..addNamedSet('guild_id') + ..addNamedSet('author_id') + ..andWhere("id = @id"); + + await _database.executeQuery(query, parameters: { 'id': tag.id, 'name': tag.name, 'content': tag.content, @@ -99,26 +94,22 @@ class TagRepository { } Future> fetchTagUsage() async { - final result = await _database.getConnection().execute(Sql.named(''' - SELECT tu.* FROM tag_usage tu JOIN tags t ON t.id = tu.command_id AND t.enabled = TRUE; - ''')); + final query = SelectQuery.selectAll("tag_usage", alias: "tu") + ..addJoin('tags', 't', ['t.id = tu.command_id', 't.enabled = TRUE']); + + final result = await _database.executeQuery(query); return result.map((row) => row.toColumnMap()).map(TagUsedEvent.fromRow); } Future registerTagUsedEvent(TagUsedEvent event) async { - await _database.getConnection().execute(Sql.named(''' - INSERT INTO tag_usage ( - command_id, - use_date, - hidden - ) VALUES ( - @tag_id, - @use_date, - @hidden - ) - '''), parameters: { - 'tag_id': event.tagId, + final query = InsertQuery("tag_usage") + ..addNamedInsert("command_id") + ..addNamedInsert("use_date") + ..addNamedInsert("hidden"); + + await _database.executeQuery(query, parameters: { + 'command_id': event.tagId, 'use_date': event.usedAt, 'hidden': event.hidden, }); diff --git a/lib/src/services/db.dart b/lib/src/services/db.dart index 95d84d5..3428e85 100644 --- a/lib/src/services/db.dart +++ b/lib/src/services/db.dart @@ -6,6 +6,7 @@ import 'package:migent/migent.dart'; import 'package:postgres/postgres.dart'; import 'package:running_on_dart/src/settings.dart'; +import 'package:running_on_dart/src/util/query_builder.dart'; import 'package:running_on_dart/src/util/util.dart'; /// The user to use when connecting to the database. @@ -163,7 +164,7 @@ class DatabaseService implements RequiresInitialization { token VARCHAR NOT NULL, jellyfin_config_id INT NOT NULL, CONSTRAINT fk_jellyfin_configs - FOREIGN KEY(jellyfin_config_id) + FOREIGN KEY(jellyfin_config_id) REFERENCES jellyfin_configs(id) ); ''') @@ -178,4 +179,8 @@ class DatabaseService implements RequiresInitialization { } Connection getConnection() => _connection; + + Future executeQuery(Query query, {Map? parameters}) { + return getConnection().execute(query.build(), parameters: parameters); + } } diff --git a/lib/src/util/query_builder.dart b/lib/src/util/query_builder.dart new file mode 100644 index 0000000..a300a26 --- /dev/null +++ b/lib/src/util/query_builder.dart @@ -0,0 +1,204 @@ +import 'package:postgres/postgres.dart'; + +class QueryBuilderException implements Exception { + final String message; + + QueryBuilderException(this.message); + + @override + String toString() => "QueryBuilderException: $message"; +} + +String buildSelects(List selects) { + if (selects.isEmpty) { + throw QueryBuilderException("No select statements provided. Select query needs at least one select statement."); + } + + return selects.join(","); +} + +String buildWheres(List wheres, String type) { + return wheres.join(" $type "); +} + +String buildSets(Map sets) { + return sets.entries.map((entry) => '${entry.key} = ${entry.value}').join(","); +} + +(String, String) buildInsert(Map inserts) { + return ( + inserts.entries.map((entry) => entry.key).join(","), + inserts.entries.map((entry) => entry.value).join(","), + ); +} + +String buildReturnings(List returnings) => returnings.join(","); + +extension ToStringCleanStringBufferExtension on StringBuffer { + String toStringClean() { + return toString().split(" ").where((substr) => substr.isNotEmpty).join(" ").replaceFirst(" ;", ";"); + } +} + +abstract class Query { + final String from; + final String? alias; + + String get aliasOrEmpty => alias ?? ''; + + Query(this.from, {this.alias}); + + Sql build(); +} + +mixin _WhereQuery implements Query { + final List _andWheres = []; + final List _orWheres = []; + + void andWhere(String expression) => _andWheres.add(expression); + void orWhere(String expression) => _orWheres.add(expression); + + void _buildWheres(StringBuffer buffer) { + if (_andWheres.isNotEmpty) { + buffer.write("WHERE "); + buffer.write(buildWheres(_andWheres, 'AND')); + } + + if (_orWheres.isNotEmpty) { + if (_andWheres.isEmpty) { + buffer.write("WHERE "); + } else { + buffer.write("OR "); + } + buffer.write(buildWheres(_orWheres, "OR")); + } + } +} + +class _Join { + final String target; + final String targetAlias; + final String joinType; // LEFT JOIN, RIGHT JOIN, JOIN, OUTER JOIN + final List conditions; + + _Join(this.target, this.targetAlias, this.joinType, this.conditions); + + String build() => "$joinType $target $targetAlias ON ${buildWheres(conditions, 'AND')}"; +} + +mixin _JoinQuery implements Query { + final List<_Join> _joins = []; + + void addJoin(String target, String alias, List conditions) => + _joins.add(_Join(target, alias, 'JOIN', conditions)); + void addLeftJoin(String target, String alias, List conditions) => + _joins.add(_Join(target, alias, 'LEFT JOIN', conditions)); + + void _buildJoins(StringBuffer buffer) { + if (_joins.isEmpty) { + return; + } + + buffer.write(_joins.map((join) => join.build()).join(",")); + } +} + +class InsertQuery extends Query { + final Map _inserts = {}; + final List _returnings = []; + + InsertQuery(super.from, {super.alias}); + + void addInsert(String name, String value) => _inserts[name] = value; + void addNamedInsert(String name) => _inserts[name] = '@$name'; + void addReturning(String name) => _returnings.add(name); + + @override + Sql build() { + final buffer = StringBuffer("INSERT INTO $from ("); + + final (fields, values) = buildInsert(_inserts); + + buffer.write(fields); + buffer.write(") VALUES ("); + buffer.write(values); + buffer.write(")"); + + if (_returnings.isNotEmpty) { + buffer.write(" RETURNING "); + buffer.write(buildReturnings(_returnings)); + } + + buffer.write(";"); + + return Sql.named(buffer.toStringClean()); + } +} + +class UpdateQuery extends Query with _WhereQuery { + final Map _sets = {}; + + UpdateQuery(super.from, {super.alias}); + + void addSet(String name, String value) => _sets[name] = value; + void addNamedSet(String name) => _sets[name] = "@$name"; + + @override + Sql build() { + if (_andWheres.isEmpty && _orWheres.isEmpty) { + throw QueryBuilderException("Update query requires where statement"); + } + + final buffer = StringBuffer("UPDATE $from SET "); + buffer.write(buildSets(_sets)); + buffer.write(" "); + + _buildWheres(buffer); + + buffer.write(";"); + return Sql.named(buffer.toStringClean()); + } +} + +class DeleteQuery extends Query with _WhereQuery { + DeleteQuery(super.from); + + @override + Sql build() { + if (_andWheres.isEmpty && _orWheres.isEmpty) { + throw QueryBuilderException("Delete query requires where statement"); + } + + final buffer = StringBuffer("DELETE FROM $from $aliasOrEmpty "); + + _buildWheres(buffer); + buffer.write(";"); + + return Sql.named(buffer.toStringClean()); + } +} + +class SelectQuery extends Query with _WhereQuery, _JoinQuery { + final List _selects = []; + + SelectQuery(super.from, {super.alias}); + factory SelectQuery.selectAll(String from, {String? alias}) => + SelectQuery(from, alias: alias)..select("${alias != null ? '$alias.' : ''}*"); + + void select(String expression) => _selects.add(expression); + + @override + Sql build() { + final buffer = StringBuffer("SELECT "); + buffer.write(buildSelects(_selects)); + buffer.write(" FROM $from ${alias ?? ""} "); + + _buildJoins(buffer); + buffer.write(" "); + _buildWheres(buffer); + + buffer.write(";"); + + return Sql.named(buffer.toStringClean()); + } +} diff --git a/lib/src/util/util.dart b/lib/src/util/util.dart index c710fc7..ee1c3d4 100644 --- a/lib/src/util/util.dart +++ b/lib/src/util/util.dart @@ -19,9 +19,9 @@ String getCurrentMemoryString() { return '$current/$rss MB'; } -String getDartPlatform() => Platform.version.split('(').first; +String getDartPlatform() => Platform.version.split('(').first.trim(); -extension DurationFromTicks on Duration { +extension FormatShortDurationExtension on Duration { String formatShort() => toString().split('.').first.padLeft(8, "0"); } @@ -52,7 +52,7 @@ Iterable spliceEmbedsForMessageBuilders(Iterable e } } -Duration? getDurationFromStringOrDefault(String? durationString, Duration? defaultDuration) { +Duration? getDurationFromStringOrDefault(String? durationString, [Duration? defaultDuration]) { if (durationString == null) { return defaultDuration; } diff --git a/pubspec.lock b/pubspec.lock index b5ffddb..46cee48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" + url: "https://pub.dev" + source: hosted + version: "1.11.0" crypto: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" fuzzy: dependency: "direct main" description: @@ -193,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -225,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" lints: dependency: "direct dev" description: @@ -245,10 +285,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" meta: dependency: transitive description: @@ -266,6 +306,22 @@ packages: url: "https://github.com/nyxx-discord/migent.git" source: git version: "1.1.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" nyxx: dependency: "direct main" description: @@ -342,10 +398,10 @@ packages: dependency: "direct main" description: name: postgres - sha256: c271fb05cf83f47ff8d6915ea7fc780381e581309f55846a21a3257ad6b05f6d + sha256: "9ce23b749d87aa0cc09b0328fe5193c86ad09d9940f920c887c6cbe85bea11cc" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.2" pub_semver: dependency: transitive description: @@ -394,6 +450,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -451,6 +555,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + url: "https://pub.dev" + source: hosted + version: "1.25.8" test_api: dependency: transitive description: @@ -459,6 +571,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" + test_core: + dependency: transitive + description: + name: test_core + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + url: "https://pub.dev" + source: hosted + version: "0.6.5" typed_data: dependency: transitive description: @@ -475,6 +595,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" watcher: dependency: transitive description: @@ -491,6 +619,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 46034f1..5bcdfe6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,3 +32,4 @@ dependencies: dev_dependencies: lints: ^5.0.0 + test: ^1.25.8 diff --git a/test/query_builder_test.dart b/test/query_builder_test.dart new file mode 100644 index 0000000..d9bdac6 --- /dev/null +++ b/test/query_builder_test.dart @@ -0,0 +1,126 @@ +import 'package:postgres/postgres.dart'; +import 'package:postgres/src/v3/query_description.dart' show SqlImpl; + +import 'package:running_on_dart/src/util/query_builder.dart'; +import 'package:test/test.dart'; + +extension PostgresSqlStringExtension on Sql { + String asString() => (this as SqlImpl).sql; +} + +void main() { + group("Query builder tests", () { + group("Select tests", () { + test("Simple select", () { + final query = SelectQuery("test") + ..select("*") + ..andWhere("name = 'test'"); + + expect(query.build().asString(), "SELECT * FROM test WHERE name = 'test';"); + }); + + test("Simple select all", () { + final query = SelectQuery.selectAll("test")..andWhere("name = 'test'"); + + expect(query.build().asString(), "SELECT * FROM test WHERE name = 'test';"); + }); + + test("Select without where", () { + final query = SelectQuery("test")..select("*"); + + expect(query.build().asString(), "SELECT * FROM test;"); + }); + + test("Select multiple and statements", () { + final query = SelectQuery("test") + ..select("*") + ..andWhere("name = 'test'") + ..andWhere("model = 'xg'"); + + expect(query.build().asString(), "SELECT * FROM test WHERE name = 'test' AND model = 'xg';"); + }); + + test("Select multiple or statements", () { + final query = SelectQuery("test") + ..select("*") + ..orWhere("name = 'test'") + ..orWhere("model = 'xg'"); + + expect(query.build().asString(), "SELECT * FROM test WHERE name = 'test' OR model = 'xg';"); + }); + + test("Join another table", () { + final query = SelectQuery("test", alias: "t") + ..select("t.*") + ..select("ot.*") + ..orWhere("t.name = 'test'") + ..orWhere("t.model = 'xg'") + ..addJoin("other_table", "ot", ["ot.id = t.test_id"]) + ..addLeftJoin("another_table", "at", ["at.test_id = t.id"]); + + expect(query.build().asString(), + "SELECT t.*,ot.* FROM test t JOIN other_table ot ON ot.id = t.test_id,LEFT JOIN another_table at ON at.test_id = t.id WHERE t.name = 'test' OR t.model = 'xg';"); + }); + + test('Join multiple conditions', () { + final query = SelectQuery.selectAll("tag_usage", alias: "tu") + ..addJoin('tags', 't', ['t.id = tu.command_id', 't.enabled = TRUE']); + + expect(query.build().asString(), + "SELECT tu.* FROM tag_usage tu JOIN tags t ON t.id = tu.command_id AND t.enabled = TRUE;"); + }); + + test("Simple select all with alias", () { + final query = SelectQuery.selectAll("test", alias: 't')..andWhere("t.name = 'test'"); + + expect(query.build().asString(), "SELECT t.* FROM test t WHERE t.name = 'test';"); + }); + }); + + group("Update tests", () { + test("Simple update", () { + final query = UpdateQuery("test") + ..addSet("name", "moron") + ..andWhere("id = 1"); + + expect(query.build().asString(), "UPDATE test SET name = moron WHERE id = 1;"); + }); + + test("Named sets", () { + final query = UpdateQuery("test") + ..addNamedSet("name") + ..addNamedSet("model") + ..andWhere("id = 1"); + + expect(query.build().asString(), "UPDATE test SET name = @name,model = @model WHERE id = 1;"); + }); + }); + + group("Insert tests", () { + test("Simple insert", () { + final query = InsertQuery("test") + ..addInsert("name", "moron") + ..addNamedInsert("model"); + + expect(query.build().asString(), "INSERT INTO test (name,model) VALUES (moron,@model);"); + }); + + test("Insert with returning", () { + final query = InsertQuery("test") + ..addInsert("name", "moron") + ..addNamedInsert("model") + ..addReturning("id"); + + expect(query.build().asString(), "INSERT INTO test (name,model) VALUES (moron,@model) RETURNING id;"); + }); + }); + + group("Delete tests", () { + test("Simple delete", () { + final query = DeleteQuery("test")..andWhere("name = 'test'"); + + expect(query.build().asString(), "DELETE FROM test WHERE name = 'test';"); + }); + }); + }); +} diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 0000000..48f668e --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,54 @@ +import 'package:running_on_dart/src/util/util.dart'; +import 'package:test/test.dart'; + +void main() { + test("random color test", () { + final color = getRandomColor(); + final secondColor = getRandomColor(); + + expect(color, isNot(secondColor)); + expect(color, color); + }); + + group("FormatShortDurationExtension", () { + test("Minutes and second", () { + final duration = Duration(minutes: 2, seconds: 56); + + expect(duration.formatShort(), '00:02:56'); + }); + + test("Hours, minutes and second", () { + final duration = Duration(hours: 10, minutes: 2, seconds: 56); + + expect(duration.formatShort(), '10:02:56'); + }); + }); + + group("valueOrNull", () { + test("value exists", () { + expect(valueOrNull('value'), 'value'); + }); + + test("whitespace", () { + expect(valueOrNull(' '), isNull); + }); + + test("null value", () { + expect(valueOrNull(null), isNull); + }); + }); + + group("getDurationFromStringOrDefault", () { + test("value exists", () { + expect(getDurationFromStringOrDefault("2 minutes"), Duration(minutes: 2)); + }); + + test("value null", () { + expect(getDurationFromStringOrDefault(null, Duration(seconds: 1)), Duration(seconds: 1)); + }); + + test("value null and default null", () { + expect(getDurationFromStringOrDefault(null, null), isNull); + }); + }); +}