Began to make unit tests + started to document CRUD service

This commit is contained in:
dan63047 2024-01-30 00:13:07 +03:00
parent 8de7946370
commit a3e09d1ca7
8 changed files with 396 additions and 23 deletions

View File

@ -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<void> 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<void> 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<void> 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<int> compressDB() async{
await ensureDbIsOpen();
final db = getDatabaseOrThrow();

View File

@ -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<String, List<TetrioPlayer>> _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<String, TetrioPlayer> _playersCache = {};
final Map<String, Map<String, dynamic>> _recordsCache = {};
final Map<String, dynamic> _replaysCache = {};
final Map<String, dynamic> _replaysCache = {}; // the only one is different: {"replayID": [replayString, replayBytes]}
final Map<String, TetrioPlayersLeaderboard> _leaderboardsCache = {};
final Map<String, List<News>> _newsCache = {};
final Map<String, Map<String, double?>> _topTRcache = {};
final Map<String, TetraLeagueAlphaStream> _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<String, TetraLeagueAlphaStream> _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<Map<String, List<TetrioPlayer>>> _tetrioStreamController;
@ -90,6 +97,7 @@ class TetrioService extends DB {
Stream<Map<String, List<TetrioPlayer>>> get allPlayers => _tetrioStreamController.stream;
/// Loading and sending to the stream everyone.
Future<void> _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<void> 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<String> 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<void> 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<List<dynamic>> 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<String, List<TetrioPlayer>> 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);

View File

@ -0,0 +1,3 @@
extension MinusOneSecond on Duration{
static const Duration minusOneSecond = Duration(seconds: -1);
}

View File

@ -196,12 +196,10 @@ class _MainState extends State<MainView> 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 = <DropdownMenuItem<List<FlSpot>>>[ // 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 = <DropdownMenuItem<List<FlSpot>>>[ // 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<MainView> with TickerProviderStateMixin {
case ConnectionState.done:
//bool bigScreen = MediaQuery.of(context).size.width > 1024;
if (snapshot.hasData) {
List<RecordSingle> sprintRuns = snapshot.data![1]['sprint'];
List<RecordSingle> blitzRuns = snapshot.data![1]['blitz'];
List<dynamic> sprintRuns = snapshot.data![1]['sprint'];
List<dynamic> blitzRuns = snapshot.data![1]['blitz'];
return RefreshIndicator(
onRefresh: () {
return Future(() => changePlayer(snapshot.data![0].userId));

View File

@ -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(

View File

@ -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:

View File

@ -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:

181
test/api_test.dart Normal file
View File

@ -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<TetrioPlayerNotExist>()));
});
});
}