diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a98f09..05b5551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2025-12-15 + +### Added + +- **Sort Lines**: New text line manipulation tool + - Sort alphabetically (A-Z, Z-A) + - Natural sort for alphanumeric strings (file1, file2, file10) + - Numeric sort (ascending/descending) + - Sort by line length + - Reverse line order + - Remove duplicates (dedupe) + - Shuffle/randomize lines + - Options: case sensitive, trim whitespace, remove empty lines + +## [1.2.1] - 2025-12-15 + +### Fixed + +- Code Editor: Fix PHP parse error when loading the page (` 'tools.diff', 'icon' => 'diff', ], + [ + 'name' => 'Sort Lines', + 'description' => 'Sort, deduplicate, reverse, and shuffle lines', + 'route' => 'tools.sort-lines', + 'icon' => 'sort', + ], ]; return view('home', compact('tools')); @@ -266,4 +272,9 @@ public function diff(): View { return view('tools.diff'); } + + public function sortLines(): View + { + return view('tools.sort-lines'); + } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d703241 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,35 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 4e1851a..972ddb3 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -151,6 +151,11 @@ @break + @case('sort') + + + + @break @endswitch
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index ca2ea7e..31ae7c0 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -126,7 +126,7 @@ class="absolute top-1 w-6 h-6 rounded-full shadow-lg transition-all duration-500 About Privacy GitHub - v1.2.0 + v1.3.0
diff --git a/resources/views/tools/code-editor.blade.php b/resources/views/tools/code-editor.blade.php index 731ebfa..09699b4 100644 --- a/resources/views/tools/code-editor.blade.php +++ b/resources/views/tools/code-editor.blade.php @@ -575,7 +575,7 @@ function getDefaultContent(language) { case 'css': return '/* Styles */\n'; case 'javascript': return '// JavaScript\n'; case 'json': return '{\n \n}'; - case 'php': return ' +{ + "@@context": "https://schema.org", + "@@type": "SoftwareApplication", + "name": "Sort Lines", + "description": "Sort, deduplicate, reverse, and shuffle text lines", + "url": "{{ route('tools.sort-lines') }}", + "applicationCategory": "DeveloperApplication", + "operatingSystem": "Any", + "offers": { + "@@type": "Offer", + "price": "0", + "priceCurrency": "USD" + }, + "author": { + "@@type": "Person", + "name": "Ghabri Djalel" + } +} + +@endpush + +@section('content') +
+
+
+

Sort Lines

+

Sort, deduplicate, reverse, and shuffle text lines

+
+ ← Back +
+ +
+
+
+
+ +
+ + +
+
+ +
+
+ lines + unique + duplicates +
+
+
+ +
+
+ + +
+ +
+
+ lines +
+
+
+
+ +
+
+

Sort Options

+
+ +
+
+ +
+

Options

+
+ + + +
+
+ +
+

Quick Actions

+
+ + + + +
+
+
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/routes/web.php b/routes/web.php index 63e04f2..de71750 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,7 @@ Route::get('/jwt', [ToolController::class, 'jwt'])->name('jwt'); Route::get('/timestamp', [ToolController::class, 'timestamp'])->name('timestamp'); Route::get('/diff', [ToolController::class, 'diff'])->name('diff'); + Route::get('/sort-lines', [ToolController::class, 'sortLines'])->name('sort-lines'); }); // Static Pages @@ -62,6 +63,7 @@ ['loc' => route('tools.jwt'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.timestamp'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.diff'), 'priority' => '0.8', 'changefreq' => 'monthly'], + ['loc' => route('tools.sort-lines'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('about'), 'priority' => '0.5', 'changefreq' => 'monthly'], ['loc' => route('privacy'), 'priority' => '0.3', 'changefreq' => 'yearly'], ]; diff --git a/tests/Feature/Api/Base64ApiTest.php b/tests/Feature/Api/Base64ApiTest.php new file mode 100644 index 0000000..7ff460c --- /dev/null +++ b/tests/Feature/Api/Base64ApiTest.php @@ -0,0 +1,116 @@ +postJson('/api/v1/base64/encode', [ + 'input' => 'Hello World', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'result' => 'SGVsbG8gV29ybGQ=', + ]); + } + + public function test_decode_text(): void + { + $response = $this->postJson('/api/v1/base64/decode', [ + 'input' => 'SGVsbG8gV29ybGQ=', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'result' => 'Hello World', + 'is_binary' => false, + ]); + } + + public function test_decode_invalid_base64(): void + { + $response = $this->postJson('/api/v1/base64/decode', [ + 'input' => '!!!invalid!!!', + ]); + + $response->assertStatus(422) + ->assertJson(['success' => false]); + } + + public function test_encode_unicode(): void + { + $response = $this->postJson('/api/v1/base64/encode', [ + 'input' => 'こんにちは', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'result' => '44GT44KT44Gr44Gh44Gv', + ]); + } + + public function test_validation_requires_input_for_encode(): void + { + $response = $this->postJson('/api/v1/base64/encode', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['input']); + } + + public function test_validation_requires_input_for_decode(): void + { + $response = $this->postJson('/api/v1/base64/decode', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['input']); + } + + public function test_encode_file(): void + { + $file = UploadedFile::fake()->create('test.txt', 1, 'text/plain'); + file_put_contents($file->getRealPath(), 'Hello World'); + + $response = $this->post('/api/v1/base64/encode-file', [ + 'file' => $file, + ], [ + 'Accept' => 'application/json', + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertStringStartsWith('data:text/plain;base64,', $response->json('result')); + } + + public function test_encode_file_validates_size(): void + { + $file = UploadedFile::fake()->create('large.txt', 6000); // 6MB, over 5MB limit + + $response = $this->post('/api/v1/base64/encode-file', [ + 'file' => $file, + ], [ + 'Accept' => 'application/json', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['file']); + } + + public function test_encode_file_requires_file(): void + { + $response = $this->post('/api/v1/base64/encode-file', [], [ + 'Accept' => 'application/json', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['file']); + } +} diff --git a/tests/Feature/Api/CsvApiTest.php b/tests/Feature/Api/CsvApiTest.php new file mode 100644 index 0000000..82e297a --- /dev/null +++ b/tests/Feature/Api/CsvApiTest.php @@ -0,0 +1,88 @@ +postJson('/api/v1/csv/convert', [ + 'csv' => "name,age\nJohn,30\nJane,25", + 'format' => 'json', + 'has_headers' => true, + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $result = json_decode($response->json('result'), true); + $this->assertCount(2, $result); + $this->assertEquals('John', $result[0]['name']); + } + + public function test_convert_csv_to_sql(): void + { + $response = $this->postJson('/api/v1/csv/convert', [ + 'csv' => "name,age\nJohn,30", + 'format' => 'sql', + 'table_name' => 'users', + 'has_headers' => true, + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertStringContainsString('INSERT INTO `users`', $response->json('result')); + } + + public function test_convert_csv_to_php(): void + { + $response = $this->postJson('/api/v1/csv/convert', [ + 'csv' => "name,age\nJohn,30", + 'format' => 'php', + 'has_headers' => true, + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertStringContainsString("'name' => 'John'", $response->json('result')); + } + + public function test_validation_requires_csv(): void + { + $response = $this->postJson('/api/v1/csv/convert', [ + 'format' => 'json', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['csv']); + } + + public function test_validation_requires_valid_format(): void + { + $response = $this->postJson('/api/v1/csv/convert', [ + 'csv' => 'test', + 'format' => 'invalid', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['format']); + } + + public function test_custom_delimiter(): void + { + $response = $this->postJson('/api/v1/csv/convert', [ + 'csv' => "name;age\nJohn;30", + 'format' => 'json', + 'delimiter' => ';', + 'has_headers' => true, + ]); + + $response->assertStatus(200); + $result = json_decode($response->json('result'), true); + $this->assertEquals('John', $result[0]['name']); + } +} diff --git a/tests/Feature/Api/MarkdownApiTest.php b/tests/Feature/Api/MarkdownApiTest.php new file mode 100644 index 0000000..88f829a --- /dev/null +++ b/tests/Feature/Api/MarkdownApiTest.php @@ -0,0 +1,66 @@ +postJson('/api/v1/markdown/convert', [ + 'markdown' => '# Hello World', + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertStringContainsString('

Hello World

', $response->json('result')); + } + + public function test_convert_with_formatting(): void + { + $response = $this->postJson('/api/v1/markdown/convert', [ + 'markdown' => 'This is **bold** and *italic*', + ]); + + $response->assertStatus(200); + $this->assertStringContainsString('bold', $response->json('result')); + $this->assertStringContainsString('italic', $response->json('result')); + } + + public function test_convert_full_page(): void + { + $response = $this->postJson('/api/v1/markdown/convert', [ + 'markdown' => '# Test', + 'full_page' => true, + 'title' => 'My Document', + ]); + + $response->assertStatus(200); + $result = $response->json('result'); + + $this->assertStringContainsString('', $result); + $this->assertStringContainsString('My Document', $result); + } + + public function test_validation_requires_markdown(): void + { + $response = $this->postJson('/api/v1/markdown/convert', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['markdown']); + } + + public function test_code_blocks_converted(): void + { + $response = $this->postJson('/api/v1/markdown/convert', [ + 'markdown' => "```php\necho 'hello';\n```", + ]); + + $response->assertStatus(200); + $this->assertStringContainsString('
', $response->json('result'));
+        // Code block has language class
+        $this->assertMatchesRegularExpression('/]*>/', $response->json('result'));
+    }
+}
diff --git a/tests/Feature/Api/SqlApiTest.php b/tests/Feature/Api/SqlApiTest.php
new file mode 100644
index 0000000..f5e4d70
--- /dev/null
+++ b/tests/Feature/Api/SqlApiTest.php
@@ -0,0 +1,81 @@
+postJson('/api/v1/sql/format', [
+            'sql' => 'SELECT id, name FROM users WHERE active = 1',
+            'mode' => 'format',
+        ]);
+
+        $response->assertStatus(200)
+            ->assertJson(['success' => true]);
+
+        $result = $response->json('result');
+        $this->assertStringContainsString("SELECT", $result);
+        $this->assertGreaterThan(1, substr_count($result, "\n"));
+    }
+
+    public function test_compress_sql(): void
+    {
+        $response = $this->postJson('/api/v1/sql/format', [
+            'sql' => "SELECT\n  id,\n  name\nFROM\n  users",
+            'mode' => 'compress',
+        ]);
+
+        $response->assertStatus(200)
+            ->assertJson(['success' => true]);
+
+        $result = $response->json('result');
+        $this->assertEquals(0, substr_count($result, "\n"));
+    }
+
+    public function test_highlight_sql(): void
+    {
+        $response = $this->postJson('/api/v1/sql/format', [
+            'sql' => 'SELECT id FROM users',
+            'mode' => 'highlight',
+        ]);
+
+        $response->assertStatus(200)
+            ->assertJson(['success' => true]);
+
+        // Highlighted should contain HTML
+        $this->assertStringContainsString('<', $response->json('result'));
+    }
+
+    public function test_default_mode_is_format(): void
+    {
+        $response = $this->postJson('/api/v1/sql/format', [
+            'sql' => 'SELECT id FROM users',
+        ]);
+
+        $response->assertStatus(200);
+        // Should be formatted (has newlines)
+        $this->assertGreaterThan(0, substr_count($response->json('result'), "\n"));
+    }
+
+    public function test_validation_requires_sql(): void
+    {
+        $response = $this->postJson('/api/v1/sql/format', []);
+
+        $response->assertStatus(422)
+            ->assertJsonValidationErrors(['sql']);
+    }
+
+    public function test_validation_requires_valid_mode(): void
+    {
+        $response = $this->postJson('/api/v1/sql/format', [
+            'sql' => 'SELECT 1',
+            'mode' => 'invalid',
+        ]);
+
+        $response->assertStatus(422)
+            ->assertJsonValidationErrors(['mode']);
+    }
+}
diff --git a/tests/Feature/Api/YamlApiTest.php b/tests/Feature/Api/YamlApiTest.php
new file mode 100644
index 0000000..8dc7c22
--- /dev/null
+++ b/tests/Feature/Api/YamlApiTest.php
@@ -0,0 +1,79 @@
+postJson('/api/v1/yaml/convert', [
+            'input' => "name: John\nage: 30",
+            'direction' => 'yaml-to-json',
+        ]);
+
+        $response->assertStatus(200)
+            ->assertJson(['success' => true]);
+
+        $result = json_decode($response->json('result'), true);
+        $this->assertEquals('John', $result['name']);
+        $this->assertEquals(30, $result['age']);
+    }
+
+    public function test_json_to_yaml(): void
+    {
+        $response = $this->postJson('/api/v1/yaml/convert', [
+            'input' => '{"name": "John", "age": 30}',
+            'direction' => 'json-to-yaml',
+        ]);
+
+        $response->assertStatus(200)
+            ->assertJson(['success' => true]);
+
+        $this->assertStringContainsString('name: John', $response->json('result'));
+    }
+
+    public function test_invalid_yaml_returns_error(): void
+    {
+        $response = $this->postJson('/api/v1/yaml/convert', [
+            'input' => "invalid: yaml: syntax:",
+            'direction' => 'yaml-to-json',
+        ]);
+
+        $response->assertStatus(422)
+            ->assertJson(['success' => false]);
+    }
+
+    public function test_invalid_json_returns_error(): void
+    {
+        $response = $this->postJson('/api/v1/yaml/convert', [
+            'input' => '{invalid}',
+            'direction' => 'json-to-yaml',
+        ]);
+
+        $response->assertStatus(422)
+            ->assertJson(['success' => false]);
+    }
+
+    public function test_validation_requires_input(): void
+    {
+        $response = $this->postJson('/api/v1/yaml/convert', [
+            'direction' => 'yaml-to-json',
+        ]);
+
+        $response->assertStatus(422)
+            ->assertJsonValidationErrors(['input']);
+    }
+
+    public function test_validation_requires_valid_direction(): void
+    {
+        $response = $this->postJson('/api/v1/yaml/convert', [
+            'input' => 'test',
+            'direction' => 'invalid',
+        ]);
+
+        $response->assertStatus(422)
+            ->assertJsonValidationErrors(['direction']);
+    }
+}
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
new file mode 100644
index 0000000..8364a84
--- /dev/null
+++ b/tests/Feature/ExampleTest.php
@@ -0,0 +1,19 @@
+get('/');
+
+        $response->assertStatus(200);
+    }
+}
diff --git a/tests/Feature/WebRoutesTest.php b/tests/Feature/WebRoutesTest.php
index d99e52e..b4c1203 100644
--- a/tests/Feature/WebRoutesTest.php
+++ b/tests/Feature/WebRoutesTest.php
@@ -41,6 +41,7 @@ public function test_home_page_displays_all_tools(): void
         $response->assertSee('JWT Decoder');
         $response->assertSee('Timestamp Converter');
         $response->assertSee('Diff Checker');
+        $response->assertSee('Sort Lines');
     }
 
     public function test_home_page_has_tool_links(): void
@@ -70,6 +71,7 @@ public function test_home_page_has_tool_links(): void
         $response->assertSee('href="' . route('tools.jwt') . '"', false);
         $response->assertSee('href="' . route('tools.timestamp') . '"', false);
         $response->assertSee('href="' . route('tools.diff') . '"', false);
+        $response->assertSee('href="' . route('tools.sort-lines') . '"', false);
     }
 
     public function test_csv_tool_page_loads(): void
@@ -516,9 +518,30 @@ public function test_diff_tool_has_required_elements(): void
         $response->assertSee('Compare');
     }
 
+    public function test_sort_lines_page_loads(): void
+    {
+        $response = $this->get('/tools/sort-lines');
+
+        $response->assertStatus(200);
+        $response->assertSee('Sort Lines');
+        $response->assertSee('Sort, deduplicate, reverse, and shuffle text lines');
+    }
+
+    public function test_sort_lines_has_required_elements(): void
+    {
+        $response = $this->get('/tools/sort-lines');
+
+        $response->assertStatus(200);
+        $response->assertSee('Input Text');
+        $response->assertSee('Sort Options');
+        $response->assertSee('Sort A-Z');
+        $response->assertSee('Remove Duplicates');
+        $response->assertSee('Shuffle');
+    }
+
     public function test_all_pages_have_navigation(): void
     {
-        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
+        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];
 
         foreach ($pages as $page) {
             $response = $this->get($page);
@@ -530,7 +553,7 @@ public function test_all_pages_have_navigation(): void
 
     public function test_all_pages_have_theme_toggle(): void
     {
-        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
+        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];
 
         foreach ($pages as $page) {
             $response = $this->get($page);
@@ -543,7 +566,7 @@ public function test_all_pages_have_theme_toggle(): void
     public function test_all_pages_load_vite_assets(): void
     {
         // Code editor uses standalone template without Vite
-        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
+        $pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];
 
         foreach ($pages as $page) {
             $response = $this->get($page);
@@ -556,7 +579,7 @@ public function test_all_pages_load_vite_assets(): void
     public function test_all_tool_pages_have_back_link(): void
     {
         // Code editor uses standalone template with home link instead of back
-        $toolPages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
+        $toolPages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];
 
         foreach ($toolPages as $page) {
             $response = $this->get($page);
@@ -609,7 +632,7 @@ public function test_api_routes_reject_get_requests(): void
 
     public function test_pages_have_csrf_token(): void
     {
-        $pages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
+        $pages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];
 
         foreach ($pages as $page) {
             $response = $this->get($page);
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..fe1ffc2
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,10 @@
+assertTrue(true);
+    }
+}
diff --git a/tests/Unit/Services/Base64ServiceTest.php b/tests/Unit/Services/Base64ServiceTest.php
new file mode 100644
index 0000000..2497254
--- /dev/null
+++ b/tests/Unit/Services/Base64ServiceTest.php
@@ -0,0 +1,137 @@
+service = new Base64Service();
+    }
+
+    public function test_encode_simple_string(): void
+    {
+        $result = $this->service->encode('Hello World');
+        $this->assertEquals('SGVsbG8gV29ybGQ=', $result);
+    }
+
+    public function test_encode_empty_string(): void
+    {
+        $result = $this->service->encode('');
+        $this->assertEquals('', $result);
+    }
+
+    public function test_encode_unicode(): void
+    {
+        $result = $this->service->encode('こんにちは');
+        $this->assertEquals('44GT44KT44Gr44Gh44Gv', $result);
+    }
+
+    public function test_encode_special_characters(): void
+    {
+        $result = $this->service->encode('Hello & ');
+        $decoded = base64_decode($result);
+        $this->assertEquals('Hello & ', $decoded);
+    }
+
+    public function test_decode_valid_base64(): void
+    {
+        $result = $this->service->decode('SGVsbG8gV29ybGQ=');
+
+        $this->assertTrue($result['success']);
+        $this->assertEquals('Hello World', $result['result']);
+        $this->assertFalse($result['is_binary']);
+    }
+
+    public function test_decode_invalid_base64(): void
+    {
+        $result = $this->service->decode('!!!invalid!!!');
+
+        $this->assertFalse($result['success']);
+        $this->assertStringContainsString('Invalid', $result['error']);
+    }
+
+    public function test_decode_detects_binary_content(): void
+    {
+        // Create base64 of binary data (null bytes)
+        $binary = base64_encode("\x00\x01\x02\x03");
+        $result = $this->service->decode($binary);
+
+        $this->assertTrue($result['success']);
+        $this->assertTrue($result['is_binary']);
+    }
+
+    public function test_decode_unicode(): void
+    {
+        $result = $this->service->decode('44GT44KT44Gr44Gh44Gv');
+
+        $this->assertTrue($result['success']);
+        $this->assertEquals('こんにちは', $result['result']);
+        $this->assertFalse($result['is_binary']);
+    }
+
+    public function test_encode_file_creates_data_url(): void
+    {
+        $content = 'Hello World';
+        $mimeType = 'text/plain';
+
+        $result = $this->service->encodeFile($content, $mimeType);
+
+        $this->assertStringStartsWith('data:text/plain;base64,', $result);
+        $this->assertStringContainsString('SGVsbG8gV29ybGQ=', $result);
+    }
+
+    public function test_encode_file_with_image_mime(): void
+    {
+        $content = 'fake image data';
+        $mimeType = 'image/png';
+
+        $result = $this->service->encodeFile($content, $mimeType);
+
+        $this->assertStringStartsWith('data:image/png;base64,', $result);
+    }
+
+    public function test_decode_data_url_valid(): void
+    {
+        $dataUrl = 'data:text/plain;base64,SGVsbG8gV29ybGQ=';
+        $result = $this->service->decodeDataUrl($dataUrl);
+
+        $this->assertTrue($result['success']);
+        $this->assertEquals('text/plain', $result['mime_type']);
+        $this->assertEquals('Hello World', $result['content']);
+        $this->assertEquals(11, $result['size']);
+    }
+
+    public function test_decode_data_url_invalid_format(): void
+    {
+        $result = $this->service->decodeDataUrl('not a data url');
+
+        $this->assertFalse($result['success']);
+        $this->assertStringContainsString('Invalid data URL', $result['error']);
+    }
+
+    public function test_decode_data_url_invalid_base64(): void
+    {
+        $dataUrl = 'data:text/plain;base64,!!!invalid!!!';
+        $result = $this->service->decodeDataUrl($dataUrl);
+
+        $this->assertFalse($result['success']);
+        $this->assertStringContainsString('Invalid Base64', $result['error']);
+    }
+
+    public function test_roundtrip_encode_decode(): void
+    {
+        $original = 'Test string with special chars: @#$%^&*()';
+        $encoded = $this->service->encode($original);
+        $decoded = $this->service->decode($encoded);
+
+        $this->assertTrue($decoded['success']);
+        $this->assertEquals($original, $decoded['result']);
+    }
+}
diff --git a/tests/Unit/Services/CsvConverterServiceTest.php b/tests/Unit/Services/CsvConverterServiceTest.php
new file mode 100644
index 0000000..e8700bd
--- /dev/null
+++ b/tests/Unit/Services/CsvConverterServiceTest.php
@@ -0,0 +1,114 @@
+service = new CsvConverterService();
+    }
+
+    public function test_parse_simple_csv(): void
+    {
+        $csv = "name,age\nJohn,30\nJane,25";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(3, $result);
+        $this->assertEquals(['name', 'age'], $result[0]);
+        $this->assertEquals(['John', '30'], $result[1]);
+        $this->assertEquals(['Jane', '25'], $result[2]);
+    }
+
+    public function test_parse_with_semicolon_delimiter(): void
+    {
+        $csv = "name;age\nJohn;30";
+        $result = $this->service->parse($csv, ';');
+
+        $this->assertCount(2, $result);
+        $this->assertEquals(['name', 'age'], $result[0]);
+        $this->assertEquals(['John', '30'], $result[1]);
+    }
+
+    public function test_to_json_with_headers(): void
+    {
+        $csv = "name,age\nJohn,30\nJane,25";
+        $result = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($result, true);
+
+        $this->assertCount(2, $decoded);
+        $this->assertEquals('John', $decoded[0]['name']);
+        $this->assertEquals('30', $decoded[0]['age']);
+        $this->assertEquals('Jane', $decoded[1]['name']);
+    }
+
+    public function test_to_json_without_headers(): void
+    {
+        $csv = "John,30\nJane,25";
+        $result = $this->service->toJson($csv, ',', false);
+        $decoded = json_decode($result, true);
+
+        $this->assertCount(2, $decoded);
+        $this->assertEquals(['John', '30'], $decoded[0]);
+    }
+
+    public function test_to_json_empty_csv(): void
+    {
+        $result = $this->service->toJson('', ',', true);
+        $this->assertEquals('[]', $result);
+    }
+
+    public function test_to_sql_with_headers(): void
+    {
+        $csv = "name,age\nJohn,30";
+        $result = $this->service->toSql($csv, 'users', ',', true);
+
+        $this->assertStringContainsString('INSERT INTO `users`', $result);
+        $this->assertStringContainsString('`name`', $result);
+        $this->assertStringContainsString('`age`', $result);
+        $this->assertStringContainsString("'John'", $result);
+        $this->assertStringContainsString("'30'", $result);
+    }
+
+    public function test_to_sql_escapes_quotes(): void
+    {
+        $csv = "name\nO'Brien";
+        $result = $this->service->toSql($csv, 'users', ',', true);
+
+        $this->assertStringContainsString("O\\'Brien", $result);
+    }
+
+    public function test_to_sql_handles_null_values(): void
+    {
+        $csv = "name,age\nJohn,";
+        $result = $this->service->toSql($csv, 'users', ',', true);
+
+        $this->assertStringContainsString('NULL', $result);
+    }
+
+    public function test_to_php_array_with_headers(): void
+    {
+        $csv = "name,age\nJohn,30";
+        $result = $this->service->toPhpArray($csv, ',', true);
+
+        $this->assertStringContainsString("'name' => 'John'", $result);
+        // Numeric values are output as numbers, not strings
+        $this->assertStringContainsString("'age' => 30", $result);
+    }
+
+    public function test_to_php_array_without_headers(): void
+    {
+        $csv = "John,30";
+        $result = $this->service->toPhpArray($csv, ',', false);
+
+        $this->assertStringContainsString("'John'", $result);
+        // Numeric values are output as numbers
+        $this->assertStringContainsString("30", $result);
+    }
+}
diff --git a/tests/Unit/Services/CsvEdgeCasesTest.php b/tests/Unit/Services/CsvEdgeCasesTest.php
new file mode 100644
index 0000000..46f13f0
--- /dev/null
+++ b/tests/Unit/Services/CsvEdgeCasesTest.php
@@ -0,0 +1,330 @@
+service = new CsvConverterService();
+    }
+
+    // ==================== Quoted Fields ====================
+
+    public function test_quoted_field_containing_comma(): void
+    {
+        $csv = "name,address\nJohn,\"123 Main St, Apt 4\"";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+        $this->assertEquals('123 Main St, Apt 4', $result[1][1]);
+    }
+
+    public function test_quoted_field_containing_newline(): void
+    {
+        // Note: This tests if the parser handles embedded newlines in quoted fields
+        $csv = "name,note\nJohn,\"Line 1\nLine 2\"";
+        $result = $this->service->parse($csv);
+
+        // The basic str_getcsv splits on newlines first, so this tests current behavior
+        $this->assertGreaterThanOrEqual(2, count($result));
+    }
+
+    public function test_double_quotes_inside_quoted_field(): void
+    {
+        $csv = "name,quote\nJohn,\"He said \"\"Hello\"\"\"";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+        // CSV standard: doubled quotes become single quotes
+        $this->assertStringContainsString('Hello', $result[1][1]);
+    }
+
+    public function test_empty_quoted_field(): void
+    {
+        $csv = "name,value\nJohn,\"\"";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+        $this->assertEquals('', $result[1][1]);
+    }
+
+    // ==================== Unicode Characters ====================
+
+    public function test_unicode_emoji(): void
+    {
+        $csv = "name,mood\nJohn,😀";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        $this->assertEquals('😀', $decoded[0]['mood']);
+    }
+
+    public function test_unicode_cjk_characters(): void
+    {
+        $csv = "name,greeting\n田中,こんにちは";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        $this->assertEquals('田中', $decoded[0]['name']);
+        $this->assertEquals('こんにちは', $decoded[0]['greeting']);
+    }
+
+    public function test_unicode_arabic(): void
+    {
+        $csv = "name,greeting\nأحمد,مرحبا";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        $this->assertEquals('أحمد', $decoded[0]['name']);
+        $this->assertEquals('مرحبا', $decoded[0]['greeting']);
+    }
+
+    public function test_unicode_in_sql_output(): void
+    {
+        $csv = "name\n田中";
+        $sql = $this->service->toSql($csv, 'users', ',', true);
+
+        $this->assertStringContainsString('田中', $sql);
+    }
+
+    // ==================== Delimiters ====================
+
+    public function test_tab_delimiter(): void
+    {
+        $csv = "name\tage\nJohn\t30";
+        $result = $this->service->parse($csv, "\t");
+
+        $this->assertCount(2, $result);
+        $this->assertEquals(['name', 'age'], $result[0]);
+        $this->assertEquals(['John', '30'], $result[1]);
+    }
+
+    public function test_pipe_delimiter(): void
+    {
+        $csv = "name|age\nJohn|30";
+        $result = $this->service->parse($csv, '|');
+
+        $this->assertCount(2, $result);
+        $this->assertEquals('John', $result[1][0]);
+    }
+
+    public function test_delimiter_in_quoted_field(): void
+    {
+        $csv = "name,value\nJohn,\"a,b,c\"";
+        $result = $this->service->parse($csv);
+
+        $this->assertEquals('a,b,c', $result[1][1]);
+    }
+
+    // ==================== Edge Cases with Empty/Whitespace ====================
+
+    public function test_empty_csv(): void
+    {
+        $result = $this->service->parse('');
+        $this->assertEmpty($result);
+    }
+
+    public function test_whitespace_only_csv(): void
+    {
+        $result = $this->service->parse("   \n   \n   ");
+        $this->assertEmpty($result);
+    }
+
+    public function test_single_row_no_data(): void
+    {
+        $csv = "name,age";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        // With only headers and hasHeaders=true, but only 1 row,
+        // it falls through to non-header mode and returns the row as data
+        $this->assertCount(1, $decoded);
+        $this->assertEquals(['name', 'age'], $decoded[0]);
+    }
+
+    public function test_trailing_newlines(): void
+    {
+        $csv = "name,age\nJohn,30\n\n\n";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+    }
+
+    public function test_leading_whitespace_in_values(): void
+    {
+        $csv = "name,age\n  John  ,  30  ";
+        $result = $this->service->parse($csv);
+
+        // Values should preserve whitespace (trimming is user's choice)
+        $this->assertCount(2, $result);
+    }
+
+    // ==================== Inconsistent Data ====================
+
+    public function test_inconsistent_column_count_more_columns(): void
+    {
+        $csv = "name,age\nJohn,30,extra";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+        $this->assertCount(3, $result[1]); // Extra column preserved
+    }
+
+    public function test_inconsistent_column_count_fewer_columns(): void
+    {
+        $csv = "name,age,city\nJohn,30";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        $this->assertCount(1, $decoded);
+        $this->assertEquals('John', $decoded[0]['name']);
+        $this->assertEquals('30', $decoded[0]['age']);
+        $this->assertNull($decoded[0]['city']);
+    }
+
+    public function test_trailing_comma(): void
+    {
+        $csv = "name,age,\nJohn,30,";
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(2, $result);
+        // Trailing comma creates empty field
+        $this->assertCount(3, $result[0]);
+    }
+
+    // ==================== Large Data ====================
+
+    public function test_very_long_field(): void
+    {
+        $longValue = str_repeat('a', 10000);
+        $csv = "name,data\nJohn,{$longValue}";
+        $result = $this->service->parse($csv);
+
+        $this->assertEquals($longValue, $result[1][1]);
+    }
+
+    public function test_many_columns(): void
+    {
+        $headers = implode(',', range(1, 100));
+        $values = implode(',', array_fill(0, 100, 'x'));
+        $csv = "{$headers}\n{$values}";
+
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(100, $result[0]);
+        $this->assertCount(100, $result[1]);
+    }
+
+    public function test_many_rows(): void
+    {
+        $rows = ["name,age"];
+        for ($i = 0; $i < 1000; $i++) {
+            $rows[] = "User{$i},{$i}";
+        }
+        $csv = implode("\n", $rows);
+
+        $result = $this->service->parse($csv);
+
+        $this->assertCount(1001, $result);
+    }
+
+    // ==================== Special Characters ====================
+
+    public function test_backslash_in_value(): void
+    {
+        $csv = "path\nC:\\Users\\John";
+        $result = $this->service->parse($csv);
+
+        $this->assertStringContainsString('\\', $result[1][0]);
+    }
+
+    public function test_sql_special_chars_escaped(): void
+    {
+        $csv = "name\nO'Brien";
+        $sql = $this->service->toSql($csv, 'users', ',', true);
+
+        // Single quotes should be escaped
+        $this->assertStringContainsString("\\'", $sql);
+    }
+
+    public function test_html_special_chars_preserved(): void
+    {
+        $csv = "code\n";
+        $json = $this->service->toJson($csv, ',', true);
+        $decoded = json_decode($json, true);
+
+        // HTML chars should be preserved in JSON (escaping is for display)
+        $this->assertStringContainsString('