diff --git a/packages/nextcloud/lib/src/api/tables/patches/1-broken-spec.json b/packages/nextcloud/lib/src/api/tables/patches/1-broken-spec.json new file mode 100644 index 00000000000..5b0c09d7292 --- /dev/null +++ b/packages/nextcloud/lib/src/api/tables/patches/1-broken-spec.json @@ -0,0 +1,52 @@ +[ + { + "op": "replace", + "path": "/paths/~1index.php~1apps~1tables~1api~11~1tables~1{tableId}~1rows~1simple/get/responses/200/content/application~1json/schema/items/type", + "value": "array" + }, + { + "op": "add", + "path": "/paths/~1index.php~1apps~1tables~1api~11~1tables~1{tableId}~1rows~1simple/get/responses/200/content/application~1json/schema/items/items", + "value": { + "type": "object" + } + }, + { + "op": "remove", + "path": "/components/schemas/Row/properties/data/properties" + }, + { + "op": "remove", + "path": "/components/schemas/Row/properties/data/required" + }, + { + "op": "remove", + "path": "/components/schemas/Row/properties/data/nullable" + }, + { + "op": "replace", + "path": "/components/schemas/Row/properties/data/type", + "value": "array" + }, + { + "op": "add", + "path": "/components/schemas/Row/properties/data/items", + "value": { + "type": "object", + "nullable": true, + "required": [ + "columnId", + "value" + ], + "properties": { + "columnId": { + "type": "integer", + "format": "int64" + }, + "value": { + "type": "object" + } + } + } + } +] diff --git a/packages/nextcloud/lib/src/api/tables/tables.openapi.dart b/packages/nextcloud/lib/src/api/tables/tables.openapi.dart index 4cfa5b03732..572c11c454b 100644 --- a/packages/nextcloud/lib/src/api/tables/tables.openapi.dart +++ b/packages/nextcloud/lib/src/api/tables/tables.openapi.dart @@ -2324,8 +2324,11 @@ class $Api1Client { /// Builds a serializer to parse the response of [$indexTableRowsSimple_Request]. @_i2.experimental - _i1.DynamiteSerializer, void> $indexTableRowsSimple_Serializer() => _i1.DynamiteSerializer( - bodyType: const FullType(BuiltList, [FullType(String)]), + _i1.DynamiteSerializer>, void> $indexTableRowsSimple_Serializer() => + _i1.DynamiteSerializer( + bodyType: const FullType(BuiltList, [ + FullType(BuiltList, [FullType(JsonObject)]), + ]), headersType: null, serializers: _$jsonSerializers, validStatuses: const {200}, @@ -2410,7 +2413,7 @@ class $Api1Client { /// See: /// * [$indexTableRowsSimple_Request] for the request send by this method. /// * [$indexTableRowsSimple_Serializer] for a converter to parse the `Response` from an executed request. - Future<_i1.DynamiteResponse, void>> indexTableRowsSimple({ + Future<_i1.DynamiteResponse>, void>> indexTableRowsSimple({ required int tableId, int? limit, int? offset, @@ -2424,7 +2427,7 @@ class $Api1Client { final _response = await _i3.Response.fromStream(_streamedResponse); final _serializer = $indexTableRowsSimple_Serializer(); - return _i1.ResponseConverter, void>(_serializer).convert(_response); + return _i1.ResponseConverter>, void>(_serializer).convert(_response); } /// Builds a serializer to parse the response of [$indexTableRows_Request]. @@ -9322,7 +9325,7 @@ sealed class $RowInterface { String get createdAt; String get lastEditBy; String get lastEditAt; - Row_Data? get data; + BuiltList get data; /// Rebuilds the instance. /// @@ -16171,11 +16174,18 @@ final Serializers _$serializers = (Serializers().toBuilder() Api1UpdateColumnRequestApplicationJsonBuilder.new, ) ..add(Api1UpdateColumnRequestApplicationJson.serializer) - ..addBuilderFactory(const FullType(BuiltList, [FullType(String)]), ListBuilder.new) + ..addBuilderFactory(const FullType(BuiltList, [FullType(JsonObject)]), ListBuilder.new) + ..addBuilderFactory( + const FullType(BuiltList, [ + FullType(BuiltList, [FullType(JsonObject)]), + ]), + ListBuilder>.new, + ) ..addBuilderFactory(const FullType(Row), RowBuilder.new) ..add(Row.serializer) ..addBuilderFactory(const FullType(Row_Data), Row_DataBuilder.new) ..add(Row_Data.serializer) + ..addBuilderFactory(const FullType(BuiltList, [FullType.nullable(Row_Data)]), ListBuilder.new) ..addBuilderFactory(const FullType(BuiltList, [FullType(Row)]), ListBuilder.new) ..addBuilderFactory( const FullType(Api1CreateRowInTableRequestApplicationJson), @@ -16580,7 +16590,8 @@ final Serializers _$serializers = (Serializers().toBuilder() ..addBuilderFactory(const FullType(Capabilities), CapabilitiesBuilder.new) ..add(Capabilities.serializer) ..addBuilderFactory(const FullType(Capabilities_Tables), Capabilities_TablesBuilder.new) - ..add(Capabilities_Tables.serializer)) + ..add(Capabilities_Tables.serializer) + ..addBuilderFactory(const FullType(BuiltList, [FullType(String)]), ListBuilder.new)) .build(); /// Serializer for all values in this library. diff --git a/packages/nextcloud/lib/src/api/tables/tables.openapi.g.dart b/packages/nextcloud/lib/src/api/tables/tables.openapi.g.dart index 5b867c56418..cae979b6d80 100644 --- a/packages/nextcloud/lib/src/api/tables/tables.openapi.g.dart +++ b/packages/nextcloud/lib/src/api/tables/tables.openapi.g.dart @@ -3364,14 +3364,10 @@ class _$RowSerializer implements StructuredSerializer { serializers.serialize(object.lastEditBy, specifiedType: const FullType(String)), 'lastEditAt', serializers.serialize(object.lastEditAt, specifiedType: const FullType(String)), + 'data', + serializers.serialize(object.data, specifiedType: const FullType(BuiltList, [FullType.nullable(Row_Data)])), ]; - Object? value; - value = object.data; - if (value != null) { - result - ..add('data') - ..add(serializers.serialize(value, specifiedType: const FullType(Row_Data))); - } + return result; } @@ -3405,7 +3401,8 @@ class _$RowSerializer implements StructuredSerializer { result.lastEditAt = serializers.deserialize(value, specifiedType: const FullType(String))! as String; break; case 'data': - result.data.replace(serializers.deserialize(value, specifiedType: const FullType(Row_Data))! as Row_Data); + result.data.replace(serializers.deserialize(value, + specifiedType: const FullType(BuiltList, [FullType.nullable(Row_Data)]))! as BuiltList); break; } } @@ -13300,8 +13297,8 @@ abstract mixin class $RowInterfaceBuilder { String? get lastEditAt; set lastEditAt(String? lastEditAt); - Row_DataBuilder get data; - set data(Row_DataBuilder? data); + ListBuilder get data; + set data(ListBuilder? data); } class _$Row extends Row { @@ -13318,7 +13315,7 @@ class _$Row extends Row { @override final String lastEditAt; @override - final Row_Data? data; + final BuiltList data; factory _$Row([void Function(RowBuilder)? updates]) => (RowBuilder()..update(updates))._build(); @@ -13329,7 +13326,7 @@ class _$Row extends Row { required this.createdAt, required this.lastEditBy, required this.lastEditAt, - this.data}) + required this.data}) : super._() { BuiltValueNullFieldError.checkNotNull(id, r'Row', 'id'); BuiltValueNullFieldError.checkNotNull(tableId, r'Row', 'tableId'); @@ -13337,6 +13334,7 @@ class _$Row extends Row { BuiltValueNullFieldError.checkNotNull(createdAt, r'Row', 'createdAt'); BuiltValueNullFieldError.checkNotNull(lastEditBy, r'Row', 'lastEditBy'); BuiltValueNullFieldError.checkNotNull(lastEditAt, r'Row', 'lastEditAt'); + BuiltValueNullFieldError.checkNotNull(data, r'Row', 'data'); } @override @@ -13413,9 +13411,9 @@ class RowBuilder implements Builder, $RowInterfaceBuilder { String? get lastEditAt => _$this._lastEditAt; set lastEditAt(covariant String? lastEditAt) => _$this._lastEditAt = lastEditAt; - Row_DataBuilder? _data; - Row_DataBuilder get data => _$this._data ??= Row_DataBuilder(); - set data(covariant Row_DataBuilder? data) => _$this._data = data; + ListBuilder? _data; + ListBuilder get data => _$this._data ??= ListBuilder(); + set data(covariant ListBuilder? data) => _$this._data = data; RowBuilder() { Row._defaults(this); @@ -13430,7 +13428,7 @@ class RowBuilder implements Builder, $RowInterfaceBuilder { _createdAt = $v.createdAt; _lastEditBy = $v.lastEditBy; _lastEditAt = $v.lastEditAt; - _data = $v.data?.toBuilder(); + _data = $v.data.toBuilder(); _$v = null; } return this; @@ -13462,12 +13460,12 @@ class RowBuilder implements Builder, $RowInterfaceBuilder { createdAt: BuiltValueNullFieldError.checkNotNull(createdAt, r'Row', 'createdAt'), lastEditBy: BuiltValueNullFieldError.checkNotNull(lastEditBy, r'Row', 'lastEditBy'), lastEditAt: BuiltValueNullFieldError.checkNotNull(lastEditAt, r'Row', 'lastEditAt'), - data: _data?.build()); + data: data.build()); } catch (_) { late String _$failedField; try { _$failedField = 'data'; - _data?.build(); + data.build(); } catch (e) { throw BuiltValueNestedFieldError(r'Row', _$failedField, e.toString()); } diff --git a/packages/nextcloud/lib/src/api/tables/tables.openapi.json b/packages/nextcloud/lib/src/api/tables/tables.openapi.json index 6f0ef451546..687110b7caa 100644 --- a/packages/nextcloud/lib/src/api/tables/tables.openapi.json +++ b/packages/nextcloud/lib/src/api/tables/tables.openapi.json @@ -370,19 +370,22 @@ "type": "string" }, "data": { - "type": "object", - "nullable": true, - "required": [ - "columnId", - "value" - ], - "properties": { - "columnId": { - "type": "integer", - "format": "int64" - }, - "value": { - "type": "object" + "type": "array", + "items": { + "type": "object", + "nullable": true, + "required": [ + "columnId", + "value" + ], + "properties": { + "columnId": { + "type": "integer", + "format": "int64" + }, + "value": { + "type": "object" + } } } } @@ -3843,7 +3846,10 @@ "schema": { "type": "array", "items": { - "type": "string" + "type": "array", + "items": { + "type": "object" + } } } } diff --git a/packages/nextcloud/packages/nextcloud_test/lib/matchers.dart b/packages/nextcloud/packages/nextcloud_test/lib/matchers.dart new file mode 100644 index 00000000000..8899d478294 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/matchers.dart @@ -0,0 +1 @@ +export 'src/matchers/matchers.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/date_matcher.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/date_matcher.dart new file mode 100644 index 00000000000..10c9d9fd532 --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/date_matcher.dart @@ -0,0 +1,75 @@ +import 'package:nextcloud/utils.dart'; +import 'package:test/expect.dart'; +import 'package:timezone/timezone.dart' as tz; + +/// Returns a matcher which matches if the match argument is within [delta] +/// of some [value]. +/// +/// In other words, this matches if the difference of the match argument and [value] +/// is greater than or equal to -[delta] and less than or equal to [delta]. +/// +/// The match argument can either be a `DateTime, `String` or `int`. +/// A `String` value is parsed into a utc date, while an `int` value is interpreted +/// as a unix timestamp (seconds since epoch). +Matcher closeToDate(DateTime value, Duration delta) => _IsCloseToDate(value, delta); + +class _IsCloseToDate extends TypeMatcher { + const _IsCloseToDate(this._value, this._delta); + + final DateTime _value; + final Duration _delta; + + static DateTime? _parse(dynamic item) { + switch (item) { + case DateTime(): + return item; + + case String(): + final date = DateTime.parse(item); + return date.toUtc().add(date.timeZoneOffset); + + case int(): + return DateTimeUtils.fromSecondsSinceEpoch(tz.UTC, item); + + default: + return null; + } + } + + @override + bool matches( + dynamic item, + Map matchState, + ) { + final date = _parse(item); + if (date == null) { + return false; + } + + var diff = date.difference(_value); + if (diff.isNegative) { + diff = -diff; + } + return diff <= _delta; + } + + @override + Description describeMismatch( + Object? item, + Description mismatchDescription, + Map matchState, + bool verbose, + ) { + final date = _parse(item); + if (date == null) { + return super.describe(mismatchDescription.add('not an ')); + } + + final diff = date.difference(_value); + return mismatchDescription.add(' differs by ').addDescriptionOf(diff); + } + + @override + Description describe(Description description) => + description.add('a time value within ').addDescriptionOf(_delta).add(' of ').addDescriptionOf(_value); +} diff --git a/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/matchers.dart b/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/matchers.dart new file mode 100644 index 00000000000..5dbb15b876a --- /dev/null +++ b/packages/nextcloud/packages/nextcloud_test/lib/src/matchers/matchers.dart @@ -0,0 +1 @@ +export 'date_matcher.dart'; diff --git a/packages/nextcloud/packages/nextcloud_test/pubspec.yaml b/packages/nextcloud/packages/nextcloud_test/pubspec.yaml index 65f0f9f9043..67a5c778512 100644 --- a/packages/nextcloud/packages/nextcloud_test/pubspec.yaml +++ b/packages/nextcloud/packages/nextcloud_test/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: process_run: ^1.0.0+1 test: ^1.24.0 test_api: ^0.7.0 + timezone: ^0.9.4 universal_io: ^2.0.0 version: ^3.0.0 diff --git a/packages/nextcloud/test/api/tables/tables_test.dart b/packages/nextcloud/test/api/tables/tables_test.dart index 42962e52d69..c03f04da6cc 100644 --- a/packages/nextcloud/test/api/tables/tables_test.dart +++ b/packages/nextcloud/test/api/tables/tables_test.dart @@ -1,5 +1,6 @@ import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/tables.dart' as tables; +import 'package:nextcloud_test/matchers.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; import 'package:timezone/timezone.dart' as tz; @@ -74,6 +75,76 @@ void main() { expect(deleteResponse.body.columnsCount, 0); }); }); + + group('index', () { + test('retrieves a list of all tables', () async { + final response = await tester.client.tables.api1.index(); + expect(response.statusCode, 200); + expect(() => response.headers, isA()); + expect(response.body, hasLength(1)); + + expect( + response.body.single, + isA() + .having((t) => t.id, 'id', 1) + .having((t) => t.title, 'title', 'Tutorial') + .having((t) => t.emoji, 'emoji', '🚀') + .having((t) => t.ownership, 'ownership', 'user1') + .having((t) => t.ownerDisplayName, 'ownerDisplayName', 'User One') + .having((t) => t.createdBy, 'createdBy', 'user1') + .having( + (t) => t.createdAt, + 'createdAt', + closeToDate(DateTime.timestamp(), const Duration(seconds: 30)), + ) + .having((t) => t.lastEditBy, 'lastEditBy', 'user1') + .having( + (t) => t.lastEditAt, + 'lastEditAt', + closeToDate(DateTime.timestamp(), const Duration(seconds: 30)), + ) + .having((t) => t.archived, 'archived', tester.version < Version(0, 7, 0) ? isNull : isFalse) + .having((t) => t.favorite, 'favorite', tester.version < Version(0, 7, 0) ? isNull : isFalse) + .having((t) => t.isShared, 'isShared', isFalse) + .having((t) => t.hasShares, 'hasShares', isFalse) + .having((t) => t.rowsCount, 'rowsCount', 5) + .having((t) => t.columnsCount, 'columnsCount', 4), + ); + }); + }); + + group('indexTableRowsSimple', () { + setUp(() async { + await tester.client.tables.api1.index(); + resetFixture(); + }); + + test('retrieves all rows of a table', () async { + final response = await tester.client.tables.api1.indexTableRowsSimple( + tableId: 1, + ); + expect(response.statusCode, 200); + expect(() => response.headers, isA()); + expect(response.body, hasLength(6)); + }); + }); + + group('indexTableRows', () { + setUp(() async { + await tester.client.tables.api1.index(); + resetFixture(); + }); + + test('retrieves all rows of a table', () async { + final response = await tester.client.tables.api1.indexTableRows( + tableId: 1, + ); + expect(response.statusCode, 200); + expect(() => response.headers, isA()); + + expect(response.body, hasLength(5)); + }); + }); }); }); } diff --git a/packages/nextcloud/test/fixtures/tables/v1/index/retrieves_a_list_of_all_tables.regexp b/packages/nextcloud/test/fixtures/tables/v1/index/retrieves_a_list_of_all_tables.regexp new file mode 100644 index 00000000000..f4e29e68453 --- /dev/null +++ b/packages/nextcloud/test/fixtures/tables/v1/index/retrieves_a_list_of_all_tables.regexp @@ -0,0 +1,3 @@ +GET http://localhost/index\.php/apps/tables/api/1/tables +accept: application/json +authorization: Basic mock \ No newline at end of file diff --git a/packages/nextcloud/test/fixtures/tables/v1/indextablerows/retrieves_all_rows_of_a_table.regexp b/packages/nextcloud/test/fixtures/tables/v1/indextablerows/retrieves_all_rows_of_a_table.regexp new file mode 100644 index 00000000000..2c04bc9854b --- /dev/null +++ b/packages/nextcloud/test/fixtures/tables/v1/indextablerows/retrieves_all_rows_of_a_table.regexp @@ -0,0 +1,3 @@ +GET http://localhost/index\.php/apps/tables/api/1/tables/1/rows +accept: application/json +authorization: Basic mock \ No newline at end of file diff --git a/packages/nextcloud/test/fixtures/tables/v1/indextablerowssimple/retrieves_all_rows_of_a_table.regexp b/packages/nextcloud/test/fixtures/tables/v1/indextablerowssimple/retrieves_all_rows_of_a_table.regexp new file mode 100644 index 00000000000..0761ee48769 --- /dev/null +++ b/packages/nextcloud/test/fixtures/tables/v1/indextablerowssimple/retrieves_all_rows_of_a_table.regexp @@ -0,0 +1,3 @@ +GET http://localhost/index\.php/apps/tables/api/1/tables/1/rows/simple +accept: application/json +authorization: Basic mock \ No newline at end of file