From 3745b39726daecb4f48c6754bea8906037ec5884 Mon Sep 17 00:00:00 2001
From: sticks <tanner@teamhydra.dev>
Date: Sat, 19 Oct 2024 14:31:02 -0500
Subject: [PATCH] Idgen dart

---
 .gitignore                      |  29 +--
 CHANGELOG.md                    |   3 +
 LICENSE                         |  21 +++
 README.md                       |  27 +++
 analysis_options.yaml           |   1 +
 example/idgen_dart_example.dart |  11 ++
 lib/src/idgen_dart_base.dart    |  40 ++++
 lib/src/idgen_http.dart         |  83 +++++++++
 lib/src/idgen_main.dart         | 145 +++++++++++++++
 lib/teamhydra_idgen.dart        |   5 +
 pubspec.yaml                    |  17 ++
 test/idgen_dart_test.dart       | 318 ++++++++++++++++++++++++++++++++
 12 files changed, 676 insertions(+), 24 deletions(-)
 create mode 100644 CHANGELOG.md
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 analysis_options.yaml
 create mode 100644 example/idgen_dart_example.dart
 create mode 100644 lib/src/idgen_dart_base.dart
 create mode 100644 lib/src/idgen_http.dart
 create mode 100644 lib/src/idgen_main.dart
 create mode 100644 lib/teamhydra_idgen.dart
 create mode 100644 pubspec.yaml
 create mode 100644 test/idgen_dart_test.dart

diff --git a/.gitignore b/.gitignore
index 3a83c2f..1b1cb36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,27 +1,8 @@
-# See https://www.dartlang.org/guides/libraries/private-files
-
-# Files and directories created by pub
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
 .dart_tool/
-.packages
-build/
-# If you're building an application, you may want to check-in your pubspec.lock
+
+# Avoid committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
 pubspec.lock
-
-# Directory created by dartdoc
-# If you don't generate documentation locally you can remove this line.
-doc/api/
-
-# dotenv environment variables file
 .env*
-
-# Avoid committing generated Javascript files:
-*.dart.js
-*.info.json      # Produced by the --dump-info flag.
-*.js             # When generated by dart2js. Don't specify *.js if your
-                 # project includes source files written in JavaScript.
-*.js_
-*.js.deps
-*.js.map
-
-.flutter-plugins
-.flutter-plugins-dependencies
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..effe43c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+- Initial version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..415ee68
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [2024] [SticksDev]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2e7e7dd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+This package allows you to generate unique and random IDs for your Flutter/dart applications using the ID generator provided
+by [team hydra](https://teamhydra.dev/).
+
+## Features
+
+This generator provides the following ID types:
+
+-   UUIDv4 generation
+-   NanoID generation (with and without custom alphabet)
+-   Word generation
+-   2FA generation
+-   Keypair generation
+-   Snowflake generation (discord-like snowflake)
+-   License key generation
+
+## Getting started
+
+You will need a valid username and token to use this package. If you do not have one and would like to use this package, please contact us on our [discord server](https://discord.gg/zira) in the `#other-support` channel and someone will assist you.
+
+## Usage
+
+TODO: Include short and useful examples for package users. Add longer examples
+to `/example` folder.
+
+```dart
+const like = 'sample';
+```
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..ea2c9e9
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
\ No newline at end of file
diff --git a/example/idgen_dart_example.dart b/example/idgen_dart_example.dart
new file mode 100644
index 0000000..89b80f0
--- /dev/null
+++ b/example/idgen_dart_example.dart
@@ -0,0 +1,11 @@
+// Import the package
+import 'package:teamhydra_idgen/teamhydra_idgen.dart';
+
+void main() async {
+  IDGen idgen = IDGen(username: 'your_username', token: 'your_token');
+
+  // Generate a new ID, in this example we are generating a UUID V4
+  IDGenResponse uuid =
+      await idgen.generateUUIDV4(); // all generations are async
+  print('Generated UUID: ${uuid.id}'); // print the generated ID
+}
diff --git a/lib/src/idgen_dart_base.dart b/lib/src/idgen_dart_base.dart
new file mode 100644
index 0000000..6203553
--- /dev/null
+++ b/lib/src/idgen_dart_base.dart
@@ -0,0 +1,40 @@
+/// Response class for IDs generated by IDGen.
+class IDGenResponse {
+  final String id;
+
+  IDGenResponse({required this.id});
+
+  factory IDGenResponse.fromJson(Map<String, dynamic> json) {
+    return IDGenResponse(
+      id: json['id'],
+    );
+  }
+}
+
+/// Response class for keypairs generated by IDGen.
+class IDKeypairResponse implements IDGenResponse {
+  @override
+  final String id;
+  final String privateID;
+
+  IDKeypairResponse({required this.id, required this.privateID});
+
+  factory IDKeypairResponse.fromJson(Map<String, dynamic> json) {
+    return IDKeypairResponse(
+      id: json['id'],
+      privateID: json['privateID'],
+    );
+  }
+}
+
+/// Custom exception for IDGen, thrown when an error occurs when generating an ID.
+class IDGenException implements Exception {
+  final String message;
+
+  IDGenException(this.message);
+
+  @override
+  String toString() {
+    return message;
+  }
+}
diff --git a/lib/src/idgen_http.dart b/lib/src/idgen_http.dart
new file mode 100644
index 0000000..e89baae
--- /dev/null
+++ b/lib/src/idgen_http.dart
@@ -0,0 +1,83 @@
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+import 'package:teamhydra_idgen/src/idgen_dart_base.dart';
+
+class IDGenHTTPWorker {
+  final Dio _dio = Dio();
+  final String _baseURL = 'https://id.hydra.workers.dev';
+
+  final String username;
+  final String token;
+
+  IDGenHTTPWorker({required this.username, required this.token});
+
+  Future<IDGenResponse> generate(
+      String type, Map<String, dynamic>? data) async {
+    try {
+      final response = await _dio.post(_baseURL,
+          data: {
+            'username': username,
+            'token': token,
+            'type': type,
+            ...?data,
+          },
+          options: Options(contentType: Headers.jsonContentType));
+
+      return IDGenResponse.fromJson(response.data);
+    } on DioException catch (e) {
+      Map<String, dynamic>? errorData;
+
+      // Do we have a response?
+      if (e.response != null) {
+        // Try decoding the response
+        try {
+          errorData = jsonDecode(e.response!.data);
+        } catch (_) {
+          // Do nothing
+        }
+      }
+
+      if (errorData != null && errorData['error'] != null) {
+        throw IDGenException(
+            'Server rejected ID generation: ${errorData['error']}');
+      } else if (errorData != null) {
+        throw IDGenException('Server rejected ID generation: $errorData');
+      } else {
+        throw IDGenException(
+            'An error occurred during generation ($type): ${e.message}');
+      }
+    } catch (e) {
+      throw IDGenException(
+          'An unknown error occurred during generation ($type): $e');
+    }
+  }
+
+  Future<IDKeypairResponse> generateKeypair() async {
+    try {
+      final response = await _dio.post(_baseURL,
+          data: {
+            'username': username,
+            'token': token,
+            'type': 'keypair',
+          },
+          options: Options(contentType: Headers.jsonContentType));
+
+      return IDKeypairResponse.fromJson(response.data);
+    } on DioException catch (e) {
+      final errorData = e.response?.data;
+      if (errorData != null && errorData['error'] != null) {
+        throw IDGenException(
+            'Server rejected ID generation: ${errorData['error']}');
+      } else if (errorData != null) {
+        throw IDGenException('Server rejected ID generation: $errorData');
+      } else {
+        throw IDGenException(
+            'An error occurred during generation (keypair): ${e.message}');
+      }
+    } catch (e) {
+      throw IDGenException(
+          'An unknown error occurred during generation (keypair): $e');
+    }
+  }
+}
diff --git a/lib/src/idgen_main.dart b/lib/src/idgen_main.dart
new file mode 100644
index 0000000..6e2ebef
--- /dev/null
+++ b/lib/src/idgen_main.dart
@@ -0,0 +1,145 @@
+import 'package:teamhydra_idgen/src/idgen_http.dart';
+import 'package:teamhydra_idgen/teamhydra_idgen.dart';
+
+/// The main class to use when generating an ID
+///
+/// All returned IDs are in the form of a [IDGenResponse] object which contains the ID.
+/// If an error occurs, an [IDGenException] is thrown.
+class IDGen {
+  final String username;
+  final String token;
+  final IDGenHTTPWorker _worker;
+
+  /// Create a new IDGen class to generate IDs
+  ///
+  /// [username] and [token] are required to authenticate with the IDGen API.
+  IDGen({required this.username, required this.token})
+      : _worker = IDGenHTTPWorker(username: username, token: token);
+
+  /// Generate a new UUID V4
+  ///
+  /// Returns a [IDGenResponse] object containing the generated ID.
+  /// Throws an [IDGenException] if an error occurs.
+  Future<IDGenResponse> generateUUIDV4() async {
+    return await _worker.generate('uuid', null);
+  }
+
+  /// Generate a new nanoID
+  ///
+  /// Returns a [IDGenResponse] object containing the generated ID.
+  /// Optionally, you can pass a [size] parameter to specify the length of the nanoID, default is 10.
+  /// You can also pass a [alphabet] parameter to specify the characters used in the nanoID, default is provided by the API.
+  ///
+  /// Throws an [IDGenException] if an error occurs.
+  ///
+  /// Throws an [ArgumentError] if the size is not between 1 and 256 or the alphabet is empty.
+  Future<IDGenResponse> generateNanoID({int? size, String? alphabet}) async {
+    // Ensure length is between 1 and 256 (if specified)
+    if (size != null && (size < 1 || size > 256)) {
+      throw ArgumentError(
+          'Cannot generate a nanoID with a length of $size, must be between 1 and 256');
+    }
+
+    // Ensure alphabet is not empty (if specified)
+    if (alphabet != null && alphabet.isEmpty) {
+      throw ArgumentError('Cannot generate a nanoID with an empty alphabet');
+    }
+
+    // Ensure alphabet is at least 3 characters long and not longer than 256
+    if (alphabet != null && (alphabet.length < 3 || alphabet.length > 256)) {
+      throw ArgumentError(
+          'Cannot generate a nanoID with an alphabet of length ${alphabet.length}, must be between 3 and 256');
+    }
+
+    return await _worker.generate('nanoid', {
+      if (size != null) 'length': size else 'length': 10,
+      if (alphabet != null) 'alphabet': alphabet,
+    });
+  }
+
+  /// Generate a 2FA code pair
+  ///
+  /// Returns a [IDGenResponse] object containing the generated ID.
+  ///
+  /// Optionally, you can pass a [length] parameter to specify the length of the 2FA code, default is 6.
+  ///
+  /// Throws an [IDGenException] if an error occurs.
+  ///
+  /// Throws an [ArgumentError] if the length is not between 1 and 256.
+  Future<IDGenResponse> generate2FACode({int? length}) async {
+    // Ensure length is between 1 and 256 (if specified)
+    if (length != null && (length < 1 || length > 256)) {
+      throw ArgumentError(
+          'Cannot generate a 2FA code with a length of $length, must be between 1 and 256');
+    }
+
+    return await _worker.generate('2fa', {
+      if (length != null) 'length': length,
+    });
+  }
+
+  /// Generate a license key
+  ///
+  /// Returns a [IDGenResponse] object containing the generated ID.
+  /// Keys are generated with a 25 character length, resulting in a 5-5-5-5-5 format.
+  ///
+  /// Throws an [IDGenException] if an error occurs.
+  Future<IDGenResponse> generateLicenseKey() async {
+    return await _worker.generate('license', null);
+  }
+
+  /// Generate word based string
+  ///
+  /// Returns a [IDGenResponse] object containing the generated ID.
+  /// Optionally, you can pass a [length] parameter to specify the length of the word based string, default is 5.
+  /// You can also pass a [separator] parameter to specify the separator used in the word based string, options are 'slug', 'title' and 'formal'. Default is 'slug'.
+  ///
+  /// Slug: lowercase words separated by hyphens
+  ///
+  /// Title: Title case words separated by spaces
+  ///
+  /// Formal: Title case words with no spaces or separators
+  ///
+  /// Throws an [IDGenException] if an error occurs.
+  ///
+  /// Throws an [ArgumentError] if the length is not between 1 and 16 or the separator is invalid.
+  Future<IDGenResponse> generateWordBasedString(
+      {int? length, String? separator}) async {
+    // Ensure length is between 1 and 256 (if specified)
+    if (length != null && (length < 1 || length > 16)) {
+      throw ArgumentError(
+          'Cannot generate a word based string with a length of $length, must be between 1 and 16');
+    }
+
+    // Ensure separator is valid (if specified)
+    if (separator != null &&
+        separator != 'slug' &&
+        separator != 'title' &&
+        separator != 'formal') {
+      throw ArgumentError(
+          'Cannot generate a word based string with an invalid separator');
+    }
+
+    return await _worker.generate('word', {
+      if (length != null) 'length': length else 'length': 5,
+      if (separator != null) 'style': separator else 'style': 'slug',
+    });
+  }
+
+  /// Generate a snowflake ID
+  ///
+  /// Returns a [IDKeypairResponse] object containing
+  ///
+  /// Throws an [IDGenException] if an error occurs.
+  Future<IDGenResponse> generateSnowflakeID() async {
+    return await _worker.generate('snowflake', null);
+  }
+
+  /// Generate a keypair
+  ///
+  /// Returns a [IDKeypairResponse] object containing the generated ID and secret key.
+  /// Throws an [IDGenException] if an error occurs.
+  Future<IDKeypairResponse> generateKeypair() async {
+    return await _worker.generateKeypair();
+  }
+}
diff --git a/lib/teamhydra_idgen.dart b/lib/teamhydra_idgen.dart
new file mode 100644
index 0000000..b24d89d
--- /dev/null
+++ b/lib/teamhydra_idgen.dart
@@ -0,0 +1,5 @@
+/// Team Hydra ID Generator Library - Generate unique IDs for your projects, using the IDGen API.
+library;
+
+export 'src/idgen_dart_base.dart';
+export 'src/idgen_main.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..3f1d88e
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,17 @@
+name: teamhydra_idgen
+description: A library for generating unique IDs using Team Hydra's ID generation service.
+version: 1.0.0
+repository: https://teamhydra.io/Sticks/idgen-dart
+
+environment:
+  sdk: ^3.5.4
+
+# Add regular dependencies here.
+dependencies:
+  dio: ^5.7.0
+  # path: ^1.8.0
+
+dev_dependencies:
+  lints: ^4.0.0
+  test: ^1.24.0
+  dotenv: ^4.2.0
diff --git a/test/idgen_dart_test.dart b/test/idgen_dart_test.dart
new file mode 100644
index 0000000..87b8edf
--- /dev/null
+++ b/test/idgen_dart_test.dart
@@ -0,0 +1,318 @@
+import 'dart:io';
+import 'package:teamhydra_idgen/teamhydra_idgen.dart';
+import 'package:test/test.dart';
+
+Map<String, String> loadRequiredVarsFromEnv() {
+  // Load the .env file from the current directory
+  final currentDir = Directory.current;
+  final envFile = File('${currentDir.path}/.env');
+
+  if (!envFile.existsSync()) {
+    throw Exception(
+        'Could not locate the .env file in the current directory (tried path: ${envFile.path})');
+  }
+
+  final lines = envFile
+      .readAsLinesSync()
+      .where((line) => line.isNotEmpty && !line.startsWith('#'))
+      .toList(); // Filter out empty lines and comments
+
+  String username = '';
+  String token = '';
+
+  // Get the username and token (IDGEN_USERNAME and IDGEN_TOKEN)
+  for (final line in lines) {
+    if (line.startsWith('IDGEN_USERNAME=')) {
+      username = line.split('=')[1].trim();
+    } else if (line.startsWith('IDGEN_TOKEN=')) {
+      token = line.split('=')[1].trim();
+    }
+  }
+
+  // Remove " from the strings
+  username = username.replaceAll('"', '');
+  token = token.replaceAll('"', '');
+
+  if (username.isEmpty || token.isEmpty) {
+    throw Exception(
+        'IDGEN_USERNAME or IDGEN_TOKEN is missing from the .env file');
+  }
+
+  return {'username': username, 'token': token};
+}
+
+void main() {
+  // Load the .env file and get the username and token
+  final env = loadRequiredVarsFromEnv();
+
+  if (env['username'] == null ||
+      env['token'] == null ||
+      env['username']!.isEmpty ||
+      env['token']!.isEmpty) {
+    print('Please provide a valid username and token in the .env file');
+    exit(1);
+  }
+
+  final username = env['username']!;
+  final token = env['token']!;
+
+  print(
+      "[IDGen] Loaded credentials for username: $username and token (last 4): ${token.substring(token.length - 4)} to run tests");
+
+  group('IDGen', () {
+    final idGen = IDGen(username: username, token: token);
+
+    // Ensure it errors when invalid user/token combo is used
+    test('Invalid user/token combo', () async {
+      final idGen = IDGen(username: 'invalid', token: 'invalid');
+
+      try {
+        await idGen.generateUUIDV4();
+        fail('Should have thrown an exception');
+      } on IDGenException catch (e) {
+        expect(e.message,
+            'Server rejected ID generation: Invalid username or token');
+      } catch (e) {
+        fail('Should have thrown an IDGenException');
+      }
+    });
+
+    // Ensure it generates a UUID V4
+    test('Generate UUID V4', () async {
+      final response = await idGen.generateUUIDV4();
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 36);
+    });
+
+    // Ensure it generates a nanoID
+    test('Generate nanoID', () async {
+      final response = await idGen.generateNanoID();
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 10);
+    });
+
+    // Ensure it generates a nanoID with a custom size
+    test('Generate nanoID with custom size', () async {
+      final response = await idGen.generateNanoID(size: 20);
+      print('[IDGen] Generated nanoID with size 20: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 20);
+    });
+
+    // Ensure it generates a nanoID with a custom alphabet
+    test('Generate nanoID with custom alphabet', () async {
+      final response =
+          await idGen.generateNanoID(alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
+
+      print('[IDGen] Generated nanoID with custom alphabet: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 10);
+    });
+
+    // All together now
+    test('Generate nanoID with custom size and alphabet', () async {
+      final response = await idGen.generateNanoID(
+          size: 20, alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
+      print(
+          '[IDGen] Generated nanoID with size 20 and custom alphabet: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 20);
+    });
+
+    // Ensure it throws an error when generating a nanoID with an empty alphabet
+    test('Generate nanoID with empty alphabet', () async {
+      try {
+        await idGen.generateNanoID(alphabet: '');
+        fail('Should have thrown an exception');
+      } on ArgumentError catch (e) {
+        expect(e.message, 'Cannot generate a nanoID with an empty alphabet');
+      } catch (e) {
+        fail('Should have thrown an ArgumentError');
+      }
+    });
+
+    // Ensure it throws an error when generating a nanoID with an invalid alphabet length
+    test('Generate nanoID with invalid alphabet length', () async {
+      try {
+        await idGen.generateNanoID(alphabet: 'AB');
+        fail('Should have thrown an exception');
+      } on ArgumentError catch (e) {
+        expect(e.message,
+            'Cannot generate a nanoID with an alphabet of length 2, must be between 3 and 256');
+      } catch (e) {
+        fail('Should have thrown an ArgumentError');
+      }
+    });
+
+    // Ensure it throws an error when generating a nanoID with an invalid size
+    test('Generate nanoID with invalid size', () async {
+      try {
+        await idGen.generateNanoID(size: 0);
+        fail('Should have thrown an exception');
+      } on ArgumentError catch (e) {
+        expect(e.message,
+            'Cannot generate a nanoID with a length of 0, must be between 1 and 256');
+      } catch (e) {
+        fail('Should have thrown an ArgumentError');
+      }
+    });
+
+    // Ensure it generates a 2FA code pair
+    test('Generate 2FA code pair', () async {
+      final response = await idGen.generate2FACode();
+      print('[IDGen] Generated 2FA code: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 6);
+    });
+
+    // Ensure it generates a 2FA code pair with a custom length
+    test('Generate 2FA code pair with custom length', () async {
+      final response = await idGen.generate2FACode(length: 10);
+      print('[IDGen] Generated 2FA code with length 10: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 10);
+    });
+
+    // Ensure it throws an error when generating a 2FA code with an invalid length
+    test('Generate 2FA code pair with invalid length', () async {
+      try {
+        await idGen.generate2FACode(length: 0);
+        fail('Should have thrown an exception');
+      } on ArgumentError catch (e) {
+        expect(e.message,
+            'Cannot generate a 2FA code with a length of 0, must be between 1 and 256');
+      } catch (e) {
+        fail('Should have thrown an ArgumentError');
+      }
+    });
+
+    // Ensure it generates a license key
+    test('Generate license key', () async {
+      final response = await idGen.generateLicenseKey();
+      print('[IDGen] Generated license key: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+      expect(response.id.length, 29);
+
+      // Make sure it follows the 5-5-5-5-5 format
+      final parts = response.id.split('-');
+      expect(parts.length, 5);
+    });
+
+    // Ensure it generates a word based string
+    test('Generate word based string', () async {
+      final response = await idGen.generateWordBasedString();
+      print('[IDGen] Generated word based string: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+    });
+
+    // Ensure it generates a word based string with a custom length
+    test('Generate word based string with custom length', () async {
+      final response = await idGen.generateWordBasedString(length: 10);
+      print(
+          '[IDGen] Generated word based string with length 10: ${response.id}');
+
+      expect(response.id, isNotNull);
+      expect(response.id, isNotEmpty);
+
+      // Should contain -'s as the default separator
+      expect(response.id.contains('-'), isTrue);
+    });
+
+    // Ensure it can generate all separators for word based strings
+    test('Generate word based string with custom separator', () async {
+      final responseSlug =
+          await idGen.generateWordBasedString(separator: 'slug');
+      print(
+          '[IDGen] Generated word based string with slug separator: ${responseSlug.id}');
+
+      final responseTitle =
+          await idGen.generateWordBasedString(separator: 'title');
+      print(
+          '[IDGen] Generated word based string with title separator: ${responseTitle.id}');
+
+      final responseFormal =
+          await idGen.generateWordBasedString(separator: 'formal');
+      print(
+          '[IDGen] Generated word based string with formal separator: ${responseFormal.id}');
+
+      expect(responseSlug.id, isNotNull);
+      expect(responseSlug.id, isNotEmpty);
+
+      expect(responseTitle.id, isNotNull);
+      expect(responseTitle.id, isNotEmpty);
+
+      expect(responseFormal.id, isNotNull);
+      expect(responseFormal.id, isNotEmpty);
+    });
+  });
+
+  // Ensure it errors when a invalid separator is used for word based strings
+  test('Generate word based string with invalid separator', () async {
+    final idGen = IDGen(username: username, token: token);
+    try {
+      await idGen.generateWordBasedString(separator: 'invalid');
+      fail('Should have thrown an exception');
+    } on ArgumentError catch (e) {
+      expect(e.message,
+          'Cannot generate a word based string with an invalid separator');
+    } catch (e) {
+      fail('Should have thrown an ArgumentError');
+    }
+  });
+
+  // Ensure it errors when a invalid length is used for word based strings
+  test('Generate word based string with invalid length', () async {
+    final idGen = IDGen(username: username, token: token);
+    try {
+      await idGen.generateWordBasedString(length: 0);
+      fail('Should have thrown an exception');
+    } on ArgumentError catch (e) {
+      expect(e.message,
+          'Cannot generate a word based string with a length of 0, must be between 1 and 16');
+    } catch (e) {
+      fail('Should have thrown an ArgumentError');
+    }
+  });
+
+  // Ensure it generates a snowflake ID
+  test('Generate snowflake ID', () async {
+    final idGen = IDGen(username: username, token: token);
+    final response = await idGen.generateSnowflakeID();
+    print('[IDGen] Generated snowflake ID: ${response.id}');
+
+    expect(response.id, isNotNull);
+    expect(response.id, isNotEmpty);
+    expect(response.id.length, 20);
+  });
+
+  // Ensure it generates a keypair
+  test('Generate keypair', () async {
+    final idGen = IDGen(username: username, token: token);
+    final response = await idGen.generateKeypair();
+    print(
+        '[IDGen] Generated keypair: ${response.id} and secret: ${response.privateID}');
+
+    expect(response.id, isNotNull);
+    expect(response.id, isNotEmpty);
+    expect(response.privateID, isNotNull);
+    expect(response.privateID, isNotEmpty);
+  });
+}