diff --git a/lib/services/sqlite_db_controller.dart b/lib/services/sqlite_db_controller.dart index f47772e..1af160c 100644 --- a/lib/services/sqlite_db_controller.dart +++ b/lib/services/sqlite_db_controller.dart @@ -9,15 +9,19 @@ import 'package:path/path.dart' show join; const String dbName = "TetraStats.db"; +/// Base class for CRUD services. Contains basic functions class DB { Database? _db; + + /// Handles opening of DB and creates tables if they not exist Future open() async { if (_db != null) { + // in order to not open DB multiple times throw DatabaseAlreadyOpen(); } try { String dbPath; - if (kIsWeb) { + if (kIsWeb) { // i hate web dbPath = dbName; } else { final docsPath = await getApplicationDocumentsDirectory(); @@ -34,6 +38,7 @@ class DB { } } + /// Handles closing of DB Future close() async { final db = _db; if (db == null) { @@ -44,6 +49,7 @@ class DB { } } + /// if we need instance of our DB, it will return it. Database getDatabaseOrThrow() { final db = _db; if (db == null) { @@ -53,6 +59,7 @@ class DB { } } + /// You can never be too sure. Although we can be 100% sure, that DB is open after executing that function Future ensureDbIsOpen() async { try { await open(); @@ -61,6 +68,7 @@ class DB { } } + /// Executes VACUUM command for our DB and returns number of bytes, that was saved with this operation Future compressDB() async{ await ensureDbIsOpen(); final db = getDatabaseOrThrow(); diff --git a/lib/services/tetrio_crud.dart b/lib/services/tetrio_crud.dart index 8ff51fd..856dde9 100644 --- a/lib/services/tetrio_crud.dart +++ b/lib/services/tetrio_crud.dart @@ -27,6 +27,7 @@ const String endContext2 = "endContext2"; const String statesCol = "jsonStates"; const String player1id = "player1id"; const String player2id = "player2id"; +/// Table, that store players data, their stats and some moments of time const String createTetrioUsersTable = ''' CREATE TABLE IF NOT EXISTS "tetrioUsers" ( "id" TEXT UNIQUE, @@ -34,12 +35,14 @@ const String createTetrioUsersTable = ''' "jsonStates" TEXT, PRIMARY KEY("id") );'''; +/// Table, that store ids of players we need keep track of const String createTetrioUsersToTrack = ''' CREATE TABLE IF NOT EXISTS "tetrioUsersToTrack" ( "id" TEXT NOT NULL UNIQUE, PRIMARY KEY("ID") ) '''; +/// Table of Tetra League matches. Each match corresponds with their own players and end contexts const String createTetrioTLRecordsTable = ''' CREATE TABLE IF NOT EXISTS "tetrioAlphaLeagueMathces" ( "id" TEXT NOT NULL UNIQUE, @@ -52,7 +55,7 @@ const String createTetrioTLRecordsTable = ''' PRIMARY KEY("id") ) '''; - +/// Table, that contains results of replay analysis in order to not analyze it more, than one time. const String createTetrioTLReplayStats = ''' CREATE TABLE IF NOT EXISTS "tetrioTLReplayStats" ( "id" TEXT NOT NULL, @@ -64,15 +67,19 @@ const String createTetrioTLReplayStats = ''' class TetrioService extends DB { Map> _players = {}; + + // I'm trying to send as less requests, as possible, so i'm caching the results of those requests. + // Usually those maps looks like this: {"cached_until_unix_milliseconds": Object} final Map _playersCache = {}; final Map> _recordsCache = {}; - final Map _replaysCache = {}; + final Map _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]} final Map _leaderboardsCache = {}; final Map> _newsCache = {}; final Map> _topTRcache = {}; - final Map _tlStreamsCache = {}; // i'm trying to respect oskware api It should look something like {"cached_until": TetrioPlayer} - final client = UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); - //final client = UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()); + final Map _tlStreamsCache = {}; + /// Thing, that sends every request to the API endpoints + final client = kDebugMode ? UserAgentClient("Kagari-chan loves osk (Tetra Stats dev build)", http.Client()) : UserAgentClient("Tetra Stats v${packageInfo.version} (dm @dan63047 if someone abuse that software)", http.Client()); + /// We should have only one instanse of this service static final TetrioService _shared = TetrioService._sharedInstance(); factory TetrioService() => _shared; late final StreamController>> _tetrioStreamController; @@ -90,6 +97,7 @@ class TetrioService extends DB { Stream>> get allPlayers => _tetrioStreamController.stream; + /// Loading and sending to the stream everyone. Future _loadPlayers() async { final allPlayers = await getAllPlayers(); try{ @@ -101,6 +109,8 @@ class TetrioService extends DB { _tetrioStreamController.add(_players); } + /// Removes player entry from tetrioUsersTable with given [id]. + /// Can throw an error is player with this id is not exist Future deletePlayer(String id) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); @@ -113,8 +123,10 @@ class TetrioService extends DB { } } + /// Gets nickname from database or requests it from API if missing. + /// Throws an exception if user not exist or request failed. Future getNicknameByID(String id) async { - if (id.length <= 16) return id; + if (id.length <= 16) return id; // nicknames can be up to 16 symbols in length, that's how i'm differentiate nickname from ids try{ return await getPlayer(id).then((value) => value.last.username); } catch (e){ @@ -122,15 +134,18 @@ class TetrioService extends DB { } } + /// Puts results of replay analysis into a tetrioTLReplayStatsTable Future saveReplayStats(ReplayData replay) async { await ensureDbIsOpen(); final db = getDatabaseOrThrow(); db.insert(tetrioTLReplayStatsTable, {idCol: replay.id, "data": jsonEncode(replay.toJson())}); } + /// Downloads replay from inoue (szy API). Requiers [replayID]. If request have + /// different from 200 statusCode, it will throw an excepction. Returns list, that contains same replay + /// as string and as binary. Future> szyGetReplay(String replayID) async { - try{ - // read from cache + try{ // read from cache var cached = _replaysCache.entries.firstWhere((element) => element.key == replayID); return cached.value; }catch (e){ @@ -138,13 +153,13 @@ class TetrioService extends DB { } Uri url; - if (kIsWeb) { + if (kIsWeb) { // Web version sends every request through my php script at the same domain, where Tetra Stats located because of CORS url = Uri.https('ts.dan63.by', 'oskware_bridge.php', {"endpoint": "tetrioReplay", "replayid": replayID}); - } else { + } else { // Actually going to hit inoue url = Uri.https('inoue.szy.lol', '/api/replay/$replayID'); } - // trying to obtain replay from download directory first + // Trying to obtain replay from download directory first if (!kIsWeb){ // can't obtain download directory on web var downloadPath = await getDownloadsDirectory(); downloadPath ??= Platform.isAndroid ? Directory("/storage/emulated/0/Download") : await getApplicationDocumentsDirectory(); @@ -158,7 +173,7 @@ class TetrioService extends DB { switch (response.statusCode) { case 200: developer.log("szyDownload: Replay downloaded", name: "services/tetrio_crud", error: response.statusCode); - _replaysCache[replayID] = [response.body, response.bodyBytes]; + _replaysCache[replayID] = [response.body, response.bodyBytes]; // Puts results into the cache return [response.body, response.bodyBytes]; case 404: throw SzyNotFound(); @@ -829,7 +844,6 @@ class TetrioService extends DB { final db = getDatabaseOrThrow(); final players = await db.query(tetrioUsersTable); Map> data = {}; - //developer.log("getAllPlayers: $players", name: "services/tetrio_crud"); return players.map((row) { // what the fuck am i doing here? var test = json.decode(row['jsonStates'] as String); diff --git a/lib/utils/extensions.dart b/lib/utils/extensions.dart new file mode 100644 index 0000000..f18ca57 --- /dev/null +++ b/lib/utils/extensions.dart @@ -0,0 +1,3 @@ +extension MinusOneSecond on Duration{ + static const Duration minusOneSecond = Duration(seconds: -1); +} \ No newline at end of file diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index b01bd3c..68ee0da 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -196,12 +196,10 @@ class _MainState extends State with TickerProviderStateMixin { if (uniqueTL.isNotEmpty && uniqueTL.last != element.tlSeason1) uniqueTL.add(element.tlSeason1); if (uniqueTL.isEmpty) uniqueTL.add(element.tlSeason1); } - try{ - compareWith = uniqueTL.toList()[uniqueTL.length - 2]; // Also i need previous Tetra League State for comparison - }on RangeError { - compareWith = null; // If can't acess it - ok then - } - chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid + // Also i need previous Tetra League State for comparison if avaliable + compareWith = uniqueTL.length >= 2 ? uniqueTL.toList().elementAtOrNull(uniqueTL.length - 2) : null; + + chartsData = >>[ // Dumping charts data into dropdown menu items, while cheking if every entry is valid DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rating)], child: Text(t.statCellNum.tr)), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.glicko!)], child: const Text("Glicko")), DropdownMenuItem(value: [for (var tl in uniqueTL) if (tl.gamesPlayed > 9) FlSpot(tl.timestamp.millisecondsSinceEpoch.toDouble(), tl.rd!)], child: const Text("Rating Deviation")), @@ -311,8 +309,8 @@ class _MainState extends State with TickerProviderStateMixin { case ConnectionState.done: //bool bigScreen = MediaQuery.of(context).size.width > 1024; if (snapshot.hasData) { - List sprintRuns = snapshot.data![1]['sprint']; - List blitzRuns = snapshot.data![1]['blitz']; + List sprintRuns = snapshot.data![1]['sprint']; + List blitzRuns = snapshot.data![1]['blitz']; return RefreshIndicator( onRefresh: () { return Future(() => changePlayer(snapshot.data![0].userId)); diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index f198e89..ad0bb05 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -271,7 +271,7 @@ class UserThingy extends StatelessWidget { playerStatLabel: t.statCellNum.hoursPlayed, isScreenBig: bigScreen, alertTitle: t.exactGametime, - alertWidgets: [Text(player.gameTime.toString(), style: TextStyle(fontFamily: "Eurostile Round Extended"),)], + alertWidgets: [Text(player.gameTime.toString(), style: const TextStyle(fontFamily: "Eurostile Round Extended"),)], higherIsBetter: true,), if (player.gamesPlayed >= 0) StatCellNum( diff --git a/pubspec.lock b/pubspec.lock index 91dd983..43f3c9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" archive: dependency: transitive description: @@ -81,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" cross_file: dependency: transitive description: @@ -301,6 +325,22 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" go_router: dependency: "direct main" description: @@ -317,6 +357,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.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: @@ -341,6 +389,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -413,6 +469,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -613,6 +693,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + 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: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -634,6 +746,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.28.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + 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: @@ -746,6 +874,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" test_api: dependency: transitive description: @@ -754,6 +890,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" typed_data: dependency: transitive description: @@ -858,6 +1002,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: transitive description: @@ -874,6 +1026,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ad81b1c..177b187 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.1 flutter_launcher_icons: "^0.13.1" + test: ^1.24.9 flutter_launcher_icons: diff --git a/test/api_test.dart b/test/api_test.dart new file mode 100644 index 0000000..e11cfe3 --- /dev/null +++ b/test/api_test.dart @@ -0,0 +1,181 @@ +import 'dart:io'; +import 'dart:math'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; +import 'package:test/test.dart'; +import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/services/crud_exceptions.dart'; +import 'package:tetra_stats/services/tetrio_crud.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + if (kIsWeb) { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfiWeb; + } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + } + test("Initialize TetrioServise", () async { + await TetrioService().open(); + }, skip: true); // a fucking MissingPluginException how does that even happening? + // i guess i will be unable to test iteractions with DB + + group("Test fetchPlayer with different players", () { + test("dan63047 (user who have activity in tetra league)", () async { + TetrioPlayer dan63047 = await TetrioService().fetchPlayer("6098518e3d5155e6ec429cdc"); + expect(dan63047.userId, "6098518e3d5155e6ec429cdc"); + expect(dan63047.registrationTime != null, true); + expect(dan63047.avatarRevision != null, true); + expect(dan63047.connections != null, true); + expect(dan63047.role, "user"); + expect(dan63047.distinguishment, null); // imagine if that one fails one day lol + expect(dan63047.tlSeason1.glicko != null, true); + //expect(dan63047.tlSeason1.rank != "z", true); lol + expect(dan63047.tlSeason1.percentileRank != "z", true); + expect(dan63047.tlSeason1.rating > -1, true); + expect(dan63047.tlSeason1.gamesPlayed > 9, true); + expect(dan63047.tlSeason1.gamesWon > 0, true); + //expect(dan63047.tlSeason1.standing, -1); + //expect(dan63047.tlSeason1.standingLocal, -1); + expect(dan63047.tlSeason1.apm != null, true); + expect(dan63047.tlSeason1.pps != null, true); + expect(dan63047.tlSeason1.vs != null, true); + expect(dan63047.tlSeason1.nerdStats != null, true); + expect(dan63047.tlSeason1.estTr != null, true); + expect(dan63047.tlSeason1.esttracc != null, true); + expect(dan63047.tlSeason1.playstyle != null, true); + }); + test("osk (sysop who have activity in tetra league)", () async { + TetrioPlayer osk = await TetrioService().fetchPlayer("5e32fc85ab319c2ab1beb07c"); + expect(osk.userId, "5e32fc85ab319c2ab1beb07c"); + expect(osk.registrationTime, null); + expect(osk.country, "XM"); + expect(osk.avatarRevision != null, true); + expect(osk.bannerRevision != null, true); + expect(osk.connections != null, true); + expect(osk.verified, true); + expect(osk.role, "sysop"); + expect(osk.distinguishment != null, true); + expect(osk.tlSeason1.glicko != null, true); + expect(osk.tlSeason1.glicko != null, true); + expect(osk.tlSeason1.rank == "z", true); + expect(osk.tlSeason1.percentileRank != "z", true); + expect(osk.tlSeason1.rating > -1, true); + expect(osk.tlSeason1.gamesPlayed > 9, true); + expect(osk.tlSeason1.gamesWon > 0, true); + expect(osk.tlSeason1.standing, -1); + expect(osk.tlSeason1.standingLocal, -1); + expect(osk.tlSeason1.apm != null, true); + expect(osk.tlSeason1.pps != null, true); + expect(osk.tlSeason1.vs != null, true); + expect(osk.tlSeason1.nerdStats != null, true); + expect(osk.tlSeason1.estTr != null, true); + expect(osk.tlSeason1.esttracc != null, true); + expect(osk.tlSeason1.playstyle != null, true); + }); + test("kagari (sysop who have zero activity)", () async { + TetrioPlayer kagari = await TetrioService().fetchPlayer("5e331c3ce24a5a3e258f7a1b"); + expect(kagari.userId, "5e331c3ce24a5a3e258f7a1b"); + expect(kagari.registrationTime, null); + expect(kagari.country, "XM"); + expect(kagari.xp, 0); + expect(kagari.gamesPlayed, -1); + expect(kagari.gamesWon, -1); + expect(kagari.gameTime, const Duration(seconds: -1)); + expect(kagari.avatarRevision != null, true); + expect(kagari.bannerRevision != null, true); + expect(kagari.connections, null); + expect(kagari.verified, true); + expect(kagari.distinguishment != null, true); + expect(kagari.distinguishment!.detail, "kagarin"); + expect(kagari.friendCount, 1); + expect(kagari.tlSeason1.glicko, null); + expect(kagari.tlSeason1.rank, "z"); + expect(kagari.tlSeason1.percentileRank, "z"); + expect(kagari.tlSeason1.rating, -1); + expect(kagari.tlSeason1.decaying, false); + expect(kagari.tlSeason1.gamesPlayed, 0); + expect(kagari.tlSeason1.gamesWon, 0); + expect(kagari.tlSeason1.standing, -1); + expect(kagari.tlSeason1.standingLocal, -1); + expect(kagari.tlSeason1.apm, null); + expect(kagari.tlSeason1.pps, null); + expect(kagari.tlSeason1.vs, null); + expect(kagari.tlSeason1.nerdStats, null); + expect(kagari.tlSeason1.estTr, null); + expect(kagari.tlSeason1.esttracc, null); + expect(kagari.tlSeason1.playstyle, null); + }); + test("furry (banned account)", () async { + TetrioPlayer furry = await TetrioService().fetchPlayer("5eea0ff69a1ba76c20347086"); + expect(furry.userId, "5eea0ff69a1ba76c20347086"); + expect(furry.registrationTime, DateTime.parse("2020-06-17T12:43:34.790Z")); + expect(furry.role, "banned"); + expect(furry.badges.isEmpty, true); + expect(furry.badstanding, false); + expect(furry.xp, 0); + expect(furry.supporterTier, 0); + expect(furry.verified, false); + expect(furry.connections, null); + expect(furry.gamesPlayed, 0); + expect(furry.gamesWon, 0); + expect(furry.gameTime, Duration.zero); + expect(furry.tlSeason1.glicko, null); + expect(furry.tlSeason1.rank, "z"); + expect(furry.tlSeason1.percentileRank, "z"); + expect(furry.tlSeason1.rating, -1); + expect(furry.tlSeason1.decaying, false); + expect(furry.tlSeason1.gamesPlayed, 0); + expect(furry.tlSeason1.gamesWon, 0); + expect(furry.tlSeason1.standing, -1); + expect(furry.tlSeason1.standingLocal, -1); + expect(furry.tlSeason1.apm, null); + expect(furry.tlSeason1.pps, null); + expect(furry.tlSeason1.vs, null); + expect(furry.tlSeason1.nerdStats, null); + expect(furry.tlSeason1.estTr, null); + expect(furry.tlSeason1.esttracc, null); + expect(furry.tlSeason1.playstyle, null); + }); + test("oskwarefan (anon account)", () async { + TetrioPlayer oskwarefan = await TetrioService().fetchPlayer("646cb8273e887a054d64febe"); + expect(oskwarefan.userId, "646cb8273e887a054d64febe"); + expect(oskwarefan.registrationTime, DateTime.parse("2023-05-23T12:57:11.481Z")); + expect(oskwarefan.role, "anon"); + expect(oskwarefan.xp > 0, true); + expect(oskwarefan.gamesPlayed > -1, true); + expect(oskwarefan.gamesWon > -1, true); + expect(oskwarefan.gameTime.isNegative, false); + expect(oskwarefan.country, null); + expect(oskwarefan.verified, false); + expect(oskwarefan.connections, null); + expect(oskwarefan.friendCount, 0); + expect(oskwarefan.tlSeason1.glicko, null); + expect(oskwarefan.tlSeason1.rank, "z"); + expect(oskwarefan.tlSeason1.percentileRank, "z"); + expect(oskwarefan.tlSeason1.rating, -1); + expect(oskwarefan.tlSeason1.decaying, true); // ??? why true? + expect(oskwarefan.tlSeason1.gamesPlayed, 0); + expect(oskwarefan.tlSeason1.gamesWon, 0); + expect(oskwarefan.tlSeason1.standing, -1); + expect(oskwarefan.tlSeason1.standingLocal, -1); + expect(oskwarefan.tlSeason1.apm, null); + expect(oskwarefan.tlSeason1.pps, null); + expect(oskwarefan.tlSeason1.vs, null); + expect(oskwarefan.tlSeason1.nerdStats, null); + expect(oskwarefan.tlSeason1.estTr, null); + expect(oskwarefan.tlSeason1.esttracc, null); + expect(oskwarefan.tlSeason1.playstyle, null); + }); + + test("not existing account", () async { + var future = TetrioService().fetchPlayer("hasdbashdbs"); + await expectLater(future, throwsA(isA())); + }); + }); +} \ No newline at end of file