test: add Pest browser testing with SQLite :memory: schema
Set up end-to-end browser testing using Pest Browser Plugin + Playwright. New v4 test suite uses SQLite :memory: database with pre-generated schema dump (database/schema/testing-schema.sql) instead of running migrations, enabling faster test startup. - Add pestphp/pest-plugin-browser dependency - Create GenerateTestingSchema command to export PostgreSQL schema to SQLite - Add .env.testing configuration for isolated test environment - Implement v4 test directory structure (Feature, Browser, Unit tests) - Update Pest skill documentation with browser testing patterns, API reference, debugging techniques, and common pitfalls - Configure phpunit.xml and tests/Pest.php for v4 suite - Update package.json and docker-compose.dev.yml for testing dependencies
This commit is contained in:
parent
0c19464db1
commit
47a3f2e2cd
20 changed files with 3887 additions and 130 deletions
|
|
@ -23,19 +23,28 @@ ## Documentation
|
|||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
## Test Directory Structure
|
||||
|
||||
### Creating Tests
|
||||
- `tests/Feature/` and `tests/Unit/` — Legacy tests (keep, don't delete)
|
||||
- `tests/v4/Feature/` — New feature tests (SQLite :memory: database)
|
||||
- `tests/v4/Browser/` — Browser tests (Pest Browser Plugin + Playwright)
|
||||
- `tests/Browser/` — Legacy Dusk browser tests (keep, don't delete)
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
New tests go in `tests/v4/`. The v4 suite uses SQLite :memory: with a schema dump (`database/schema/testing-schema.sql`) instead of running migrations.
|
||||
|
||||
### Test Organization
|
||||
Do NOT remove tests without approval.
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
## Running Tests
|
||||
|
||||
### Basic Test Structure
|
||||
- All v4 tests: `php artisan test --compact tests/v4/`
|
||||
- Browser tests: `php artisan test --compact tests/v4/Browser/`
|
||||
- Feature tests: `php artisan test --compact tests/v4/Feature/`
|
||||
- Specific file: `php artisan test --compact tests/v4/Browser/LoginTest.php`
|
||||
- Filter: `php artisan test --compact --filter=testName`
|
||||
- Headed (see browser): `./vendor/bin/pest tests/v4/Browser/ --headed`
|
||||
- Debug (pause on failure): `./vendor/bin/pest tests/v4/Browser/ --debug`
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
|
||||
|
|
@ -45,24 +54,10 @@ ### Basic Test Structure
|
|||
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<code-snippet name="Pest Response Assertion" lang="php">
|
||||
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
|
|
@ -75,7 +70,7 @@ ## Mocking
|
|||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
Use datasets for repetitive tests:
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
|
||||
|
|
@ -88,73 +83,94 @@ ## Datasets
|
|||
|
||||
</code-snippet>
|
||||
|
||||
## Pest 4 Features
|
||||
## Browser Testing (Pest Browser Plugin + Playwright)
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
Browser tests use `pestphp/pest-plugin-browser` with Playwright. They run **outside Docker** — the plugin starts an in-process HTTP server and Playwright browser automatically.
|
||||
|
||||
### Browser Test Example
|
||||
### Key Rules
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
1. **Always use `RefreshDatabase`** — the in-process server uses SQLite :memory:
|
||||
2. **Always seed `InstanceSettings::create(['id' => 0])` in `beforeEach`** — most pages crash without it
|
||||
3. **Use `User::factory()` for auth tests** — create users with `id => 0` for root user
|
||||
4. **No Dusk, no Selenium** — use `visit()`, `fill()`, `click()`, `assertSee()` from the Pest Browser API
|
||||
5. **Place tests in `tests/v4/Browser/`**
|
||||
6. **Views with bare `function` declarations** will crash on the second request in the same process — wrap with `function_exists()` guard if you encounter this
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
### Browser Test Template
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
<code-snippet name="Coolify Browser Test Template" lang="php">
|
||||
<?php
|
||||
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0]);
|
||||
});
|
||||
|
||||
it('can visit the page', function () {
|
||||
$page = visit('/login');
|
||||
|
||||
$page->assertSee('Login');
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Smoke Testing
|
||||
### Browser Test with Form Interaction
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
<code-snippet name="Browser Test Form Example" lang="php">
|
||||
it('fails login with invalid credentials', function () {
|
||||
User::factory()->create([
|
||||
'id' => 0,
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
$page = visit('/login');
|
||||
|
||||
$page->fill('email', 'random@email.com')
|
||||
->fill('password', 'wrongpassword123')
|
||||
->click('Login')
|
||||
->assertSee('These credentials do not match our records');
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Visual Regression Testing
|
||||
### Browser API Reference
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `visit('/path')` | Navigate to a page |
|
||||
| `->fill('field', 'value')` | Fill an input by name |
|
||||
| `->click('Button Text')` | Click a button/link by text |
|
||||
| `->assertSee('text')` | Assert visible text |
|
||||
| `->assertDontSee('text')` | Assert text is not visible |
|
||||
| `->assertPathIs('/path')` | Assert current URL path |
|
||||
| `->assertSeeIn('.selector', 'text')` | Assert text in element |
|
||||
| `->screenshot()` | Capture screenshot |
|
||||
| `->debug()` | Pause test, keep browser open |
|
||||
| `->wait(seconds)` | Wait N seconds |
|
||||
|
||||
### Test Sharding
|
||||
### Debugging
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
- Screenshots auto-saved to `tests/Browser/Screenshots/` on failure
|
||||
- `->debug()` pauses and keeps browser open (press Enter to continue)
|
||||
- `->screenshot()` captures state at any point
|
||||
- `--headed` flag shows browser, `--debug` pauses on failure
|
||||
|
||||
### Architecture Testing
|
||||
## SQLite Testing Setup
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
v4 tests use SQLite :memory: instead of PostgreSQL. Schema loaded from `database/schema/testing-schema.sql`.
|
||||
|
||||
### Regenerating the Schema
|
||||
|
||||
When migrations change, regenerate from the running PostgreSQL database:
|
||||
|
||||
```bash
|
||||
docker exec coolify php artisan schema:generate-testing
|
||||
```
|
||||
|
||||
## Architecture Testing
|
||||
|
||||
<code-snippet name="Architecture Test Example" lang="php">
|
||||
|
||||
|
|
@ -172,3 +188,6 @@ ## Common Pitfalls
|
|||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- **Browser tests: forgetting `InstanceSettings::create(['id' => 0])` — most pages crash without it**
|
||||
- **Browser tests: forgetting `RefreshDatabase` — SQLite :memory: starts empty**
|
||||
- **Browser tests: views with bare `function` declarations crash on second request — wrap with `function_exists()` guard**
|
||||
|
|
|
|||
15
.env.testing
Normal file
15
.env.testing
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
APP_ENV=testing
|
||||
APP_KEY=base64:8VEfVNVkXQ9mH2L33WBWNMF4eQ0BWD5CTzB8mIxcl+k=
|
||||
APP_DEBUG=true
|
||||
|
||||
DB_CONNECTION=testing
|
||||
|
||||
CACHE_DRIVER=array
|
||||
SESSION_DRIVER=array
|
||||
QUEUE_CONNECTION=sync
|
||||
MAIL_MAILER=array
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
|
||||
SELF_HOSTED=true
|
||||
222
app/Console/Commands/GenerateTestingSchema.php
Normal file
222
app/Console/Commands/GenerateTestingSchema.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenerateTestingSchema extends Command
|
||||
{
|
||||
protected $signature = 'schema:generate-testing {--connection=pgsql : The database connection to read from}';
|
||||
|
||||
protected $description = 'Generate SQLite testing schema from the PostgreSQL database';
|
||||
|
||||
private array $typeMap = [
|
||||
'/\bbigint\b/' => 'INTEGER',
|
||||
'/\binteger\b/' => 'INTEGER',
|
||||
'/\bsmallint\b/' => 'INTEGER',
|
||||
'/\bboolean\b/' => 'INTEGER',
|
||||
'/character varying\(\d+\)/' => 'TEXT',
|
||||
'/timestamp\(\d+\) without time zone/' => 'TEXT',
|
||||
'/timestamp\(\d+\) with time zone/' => 'TEXT',
|
||||
'/\bjsonb\b/' => 'TEXT',
|
||||
'/\bjson\b/' => 'TEXT',
|
||||
'/\buuid\b/' => 'TEXT',
|
||||
'/double precision/' => 'REAL',
|
||||
'/numeric\(\d+,\d+\)/' => 'REAL',
|
||||
'/\bdate\b/' => 'TEXT',
|
||||
];
|
||||
|
||||
private array $castRemovals = [
|
||||
'::character varying',
|
||||
'::text',
|
||||
'::integer',
|
||||
'::boolean',
|
||||
'::timestamp without time zone',
|
||||
'::timestamp with time zone',
|
||||
'::numeric',
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$connection = $this->option('connection');
|
||||
|
||||
if (DB::connection($connection)->getDriverName() !== 'pgsql') {
|
||||
$this->error("Connection '{$connection}' is not PostgreSQL.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Reading schema from PostgreSQL...');
|
||||
|
||||
$tables = $this->getTables($connection);
|
||||
$lastMigration = DB::connection($connection)
|
||||
->table('migrations')
|
||||
->orderByDesc('id')
|
||||
->value('migration');
|
||||
|
||||
$output = [];
|
||||
$output[] = '-- Generated by: php artisan schema:generate-testing';
|
||||
$output[] = '-- Date: '.now()->format('Y-m-d H:i:s');
|
||||
$output[] = '-- Last migration: '.($lastMigration ?? 'none');
|
||||
$output[] = '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$columns = $this->getColumns($connection, $table);
|
||||
$output[] = $this->generateCreateTable($table, $columns);
|
||||
}
|
||||
|
||||
$indexes = $this->getIndexes($connection, $tables);
|
||||
foreach ($indexes as $index) {
|
||||
$output[] = $index;
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
$output[] = '-- Migration records';
|
||||
|
||||
$migrations = DB::connection($connection)->table('migrations')->orderBy('id')->get();
|
||||
foreach ($migrations as $m) {
|
||||
$migration = str_replace("'", "''", $m->migration);
|
||||
$output[] = "INSERT INTO \"migrations\" (\"id\", \"migration\", \"batch\") VALUES ({$m->id}, '{$migration}', {$m->batch});";
|
||||
}
|
||||
|
||||
$path = database_path('schema/testing-schema.sql');
|
||||
|
||||
if (! is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($path, implode("\n", $output)."\n");
|
||||
|
||||
$this->info("Schema written to {$path}");
|
||||
$this->info(count($tables).' tables, '.count($migrations).' migration records.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getTables(string $connection): array
|
||||
{
|
||||
return collect(DB::connection($connection)->select(
|
||||
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
|
||||
))->pluck('tablename')->toArray();
|
||||
}
|
||||
|
||||
private function getColumns(string $connection, string $table): array
|
||||
{
|
||||
return DB::connection($connection)->select(
|
||||
"SELECT column_name, data_type, character_maximum_length, column_default,
|
||||
is_nullable, udt_name, numeric_precision, numeric_scale, datetime_precision
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = ?
|
||||
ORDER BY ordinal_position",
|
||||
[$table]
|
||||
);
|
||||
}
|
||||
|
||||
private function generateCreateTable(string $table, array $columns): string
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$lines[] = ' '.$this->generateColumnDef($table, $col);
|
||||
}
|
||||
|
||||
return "CREATE TABLE IF NOT EXISTS \"{$table}\" (\n".implode(",\n", $lines)."\n);\n";
|
||||
}
|
||||
|
||||
private function generateColumnDef(string $table, object $col): string
|
||||
{
|
||||
$name = $col->column_name;
|
||||
$sqliteType = $this->convertType($col);
|
||||
|
||||
// Auto-increment primary key for id columns
|
||||
if ($name === 'id' && $sqliteType === 'INTEGER' && $col->is_nullable === 'NO' && str_contains((string) $col->column_default, 'nextval')) {
|
||||
return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL";
|
||||
}
|
||||
|
||||
$parts = ["\"{$name}\"", $sqliteType];
|
||||
|
||||
// Default value
|
||||
$default = $col->column_default;
|
||||
if ($default !== null && ! str_contains($default, 'nextval')) {
|
||||
$default = $this->cleanDefault($default);
|
||||
$parts[] = "DEFAULT {$default}";
|
||||
}
|
||||
|
||||
// NOT NULL
|
||||
if ($col->is_nullable === 'NO') {
|
||||
$parts[] = 'NOT NULL';
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
private function convertType(object $col): string
|
||||
{
|
||||
$pgType = $col->data_type;
|
||||
|
||||
return match (true) {
|
||||
in_array($pgType, ['bigint', 'integer', 'smallint']) => 'INTEGER',
|
||||
$pgType === 'boolean' => 'INTEGER',
|
||||
in_array($pgType, ['character varying', 'text', 'USER-DEFINED']) => 'TEXT',
|
||||
str_contains($pgType, 'timestamp') => 'TEXT',
|
||||
in_array($pgType, ['json', 'jsonb']) => 'TEXT',
|
||||
$pgType === 'uuid' => 'TEXT',
|
||||
$pgType === 'double precision' => 'REAL',
|
||||
$pgType === 'numeric' => 'REAL',
|
||||
$pgType === 'date' => 'TEXT',
|
||||
default => 'TEXT',
|
||||
};
|
||||
}
|
||||
|
||||
private function cleanDefault(string $default): string
|
||||
{
|
||||
foreach ($this->castRemovals as $cast) {
|
||||
$default = str_replace($cast, '', $default);
|
||||
}
|
||||
|
||||
// Remove array type casts like ::text[]
|
||||
$default = preg_replace('/::[\w\s]+(\[\])?/', '', $default);
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function getIndexes(string $connection, array $tables): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$indexes = DB::connection($connection)->select(
|
||||
"SELECT indexname, tablename, indexdef FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename, indexname"
|
||||
);
|
||||
|
||||
foreach ($indexes as $idx) {
|
||||
$def = $idx->indexdef;
|
||||
|
||||
// Skip primary key indexes
|
||||
if (str_contains($def, '_pkey')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip PG-specific indexes (GIN, GIST, expression indexes)
|
||||
if (preg_match('/USING (gin|gist)/i', $def)) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($def, '->>') || str_contains($def, '::')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to SQLite-compatible CREATE INDEX
|
||||
$unique = str_contains($def, 'UNIQUE') ? 'UNIQUE ' : '';
|
||||
|
||||
// Extract columns from the index definition
|
||||
if (preg_match('/\((.+)\)$/', $def, $m)) {
|
||||
$cols = $m[1];
|
||||
$results[] = "CREATE {$unique}INDEX IF NOT EXISTS \"{$idx->indexname}\" ON \"{$idx->tablename}\" ({$cols});";
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@
|
|||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.8.3",
|
||||
"pestphp/pest": "^4.3.2",
|
||||
"pestphp/pest-plugin-browser": "^4.2",
|
||||
"phpstan/phpstan": "^2.1.38",
|
||||
"rector/rector": "^2.3.5",
|
||||
"serversideup/spin": "^3.1.1",
|
||||
|
|
|
|||
1565
composer.lock
generated
1565
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -54,18 +54,10 @@
|
|||
],
|
||||
|
||||
'testing' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DATABASE_TEST_URL'),
|
||||
'host' => env('DB_TEST_HOST', 'postgres'),
|
||||
'port' => env('DB_TEST_PORT', '5432'),
|
||||
'database' => env('DB_TEST_DATABASE', 'coolify_test'),
|
||||
'username' => env('DB_TEST_USERNAME', 'coolify'),
|
||||
'password' => env('DB_TEST_PASSWORD', 'password'),
|
||||
'charset' => 'utf8',
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'foreign_key_constraints' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ class AddIndexToActivityLog extends Migration
|
|||
{
|
||||
public function up()
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb');
|
||||
DB::statement('CREATE INDEX idx_activity_type_uuid ON activity_log USING GIN (properties jsonb_path_ops)');
|
||||
|
|
@ -18,6 +22,10 @@ public function up()
|
|||
|
||||
public function down()
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@
|
|||
|
||||
public function up(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
||||
if (! $this->indexExists($indexName)) {
|
||||
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
||||
|
|
@ -32,6 +36,10 @@ public function up(): void
|
|||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->indexes as [, , $indexName]) {
|
||||
DB::statement("DROP INDEX IF EXISTS \"{$indexName}\"");
|
||||
}
|
||||
|
|
|
|||
1754
database/schema/testing-schema.sql
Normal file
1754
database/schema/testing-schema.sql
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,6 +26,8 @@ services:
|
|||
volumes:
|
||||
- .:/var/www/html/:cached
|
||||
- dev_backups_data:/var/www/html/storage/app/backups
|
||||
networks:
|
||||
- coolify
|
||||
postgres:
|
||||
pull_policy: always
|
||||
ports:
|
||||
|
|
|
|||
47
package-lock.json
generated
47
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
|||
"@tailwindcss/typography": "0.5.16",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ioredis": "5.6.1"
|
||||
"ioredis": "5.6.1",
|
||||
"playwright": "^1.58.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
|
|
@ -2338,6 +2339,50 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"@tailwindcss/typography": "0.5.16",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ioredis": "5.6.1"
|
||||
"ioredis": "5.6.1",
|
||||
"playwright": "^1.58.2"
|
||||
}
|
||||
}
|
||||
15
phpunit.xml
15
phpunit.xml
|
|
@ -7,17 +7,20 @@
|
|||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
<testsuite name="v4">
|
||||
<directory suffix="Test.php">./tests/v4</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage/>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="testing"/>
|
||||
<env name="DB_TEST_DATABASE" value="coolify"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="CACHE_DRIVER" value="array" force="true"/>
|
||||
<env name="DB_CONNECTION" value="testing" force="true"/>
|
||||
|
||||
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
if (! function_exists('getOldOrLocal')) {
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||
}
|
||||
}
|
||||
|
||||
$name = getOldOrLocal('name', 'test3 normal user');
|
||||
|
|
|
|||
|
|
@ -2681,24 +2681,6 @@
|
|||
"minversion": "0.0.0",
|
||||
"port": "8065"
|
||||
},
|
||||
"maybe": {
|
||||
"documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
|
||||
"slogan": "Maybe, the OS for your personal finances.",
|
||||
"compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX01BWUJFCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtICdSQUlMU19GT1JDRV9TU0w9JHtSQUlMU19GT1JDRV9TU0w6LWZhbHNlfScKICAgICAgLSAnUkFJTFNfQVNTVU1FX1NTTD0ke1JBSUxTX0FTU1VNRV9TU0w6LWZhbHNlfScKICAgICAgLSAnR09PRF9KT0JfRVhFQ1VUSU9OX01PREU9JHtHT09EX0pPQl9FWEVDVVRJT05fTU9ERTotYXN5bmN9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9tYXliZS1maW5hbmNlL21heWJlOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdidW5kbGUgZXhlYyBzaWRla2lxJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVEtFWUJBU0V9JwogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL2RlZmF1bHQ6JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfUByZWRpczo2Mzc5LzEnCiAgICAgIC0gJ09QRU5BSV9BQ0NFU1NfVE9LRU49JHtPUEVOQUlfQUNDRVNTX1RPS0VOfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBleGNsdWRlX2Zyb21faGM6IHRydWUKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbWF5YmVfcG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1heWJlLWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6OC1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMgLS1yZXF1aXJlcGFzcyAke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdSRURJU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUkVESVN9JwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFJFRElTX0RCPTEKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctLXBhc3MnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiAzCg==",
|
||||
"tags": [
|
||||
"finances",
|
||||
"wallets",
|
||||
"coins",
|
||||
"stocks",
|
||||
"investments",
|
||||
"open",
|
||||
"source"
|
||||
],
|
||||
"category": "productivity",
|
||||
"logo": "svgs/maybe.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"mealie": {
|
||||
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
|
||||
"slogan": "A recipe manager and meal planner.",
|
||||
|
|
@ -4548,6 +4530,22 @@
|
|||
"minversion": "0.0.0",
|
||||
"port": "3567"
|
||||
},
|
||||
"sure": {
|
||||
"documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
|
||||
"slogan": "An all-in-one personal finance platform.",
|
||||
"compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1NVUkVfMzAwMAogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICB2YWxrZXk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby93ZS1wcm9taXNlL3N1cmU6MC42LjcnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtIEFQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9TVVJFCiAgICAgIC0gU0VDUkVUX0tFWV9CQVNFPSRTRVJWSUNFX0JBU0U2NF9CQVNFCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1zdXJlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vdmFsa2V5OjYzNzknCiAgICAgIC0gREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9SVD01NDMyCiAgICAgIC0gU0VMRl9IT1NURUQ9dHJ1ZQogICAgICAtIFJBSUxTX0ZPUkNFX1NTTD1mYWxzZQogICAgICAtIFJBSUxTX0FTU1VNRV9TU0w9ZmFsc2UKICAgICAgLSAnT05CT0FSRElOR19TVEFURT0ke09OQk9BUkRJTkdfU1RBVEU6LW9wZW59JwogICAgdm9sdW1lczoKICAgICAgLSAnc3VyZS1hcHAtc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDE1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdmFsa2V5OgogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3ZhbGtleS1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtdmFsa2V5Oi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd2YWxrZXktY2xpIHBpbmcgfCBncmVwIFBPTkcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgICAgIHN0YXJ0X3BlcmlvZDogM3MK",
|
||||
"tags": [
|
||||
"budgeting",
|
||||
"budget",
|
||||
"money",
|
||||
"expenses",
|
||||
"income"
|
||||
],
|
||||
"category": "finance",
|
||||
"logo": "svgs/sure.png",
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"swetrix": {
|
||||
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
|
||||
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
|
||||
|
|
|
|||
|
|
@ -2681,24 +2681,6 @@
|
|||
"minversion": "0.0.0",
|
||||
"port": "8065"
|
||||
},
|
||||
"maybe": {
|
||||
"documentation": "https://github.com/maybe-finance/maybe?utm_source=coolify.io",
|
||||
"slogan": "Maybe, the OS for your personal finances.",
|
||||
"compose": "c2VydmljZXM6CiAgbWF5YmU6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdhcHBfc3RvcmFnZTovcmFpbHMvc3RvcmFnZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NQVlCRQogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSAnUkFJTFNfRk9SQ0VfU1NMPSR7UkFJTFNfRk9SQ0VfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ1JBSUxTX0FTU1VNRV9TU0w9JHtSQUlMU19BU1NVTUVfU1NMOi1mYWxzZX0nCiAgICAgIC0gJ0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFPSR7R09PRF9KT0JfRVhFQ1VUSU9OX01PREU6LWFzeW5jfScKICAgICAgLSAnU0VDUkVUX0tFWV9CQVNFPSR7U0VSVklDRV9CQVNFNjRfNjRfU0VDUkVUS0VZQkFTRX0nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjMwMDAnCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vbWF5YmUtZmluYW5jZS9tYXliZTpsYXRlc3QnCiAgICBjb21tYW5kOiAnYnVuZGxlIGV4ZWMgc2lkZWtpcScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWF5YmUtZGJ9JwogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRVJWSUNFX0JBU0U2NF82NF9TRUNSRVRLRVlCQVNFfScKICAgICAgLSBTRUxGX0hPU1RFRD10cnVlCiAgICAgIC0gJ1JBSUxTX0ZPUkNFX1NTTD0ke1JBSUxTX0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdSQUlMU19BU1NVTUVfU1NMPSR7UkFJTFNfQVNTVU1FX1NTTDotZmFsc2V9JwogICAgICAtICdHT09EX0pPQl9FWEVDVVRJT05fTU9ERT0ke0dPT0RfSk9CX0VYRUNVVElPTl9NT0RFOi1hc3luY30nCiAgICAgIC0gREJfSE9TVD1wb3N0Z3JlcwogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9kZWZhdWx0OiR7U0VSVklDRV9QQVNTV09SRF9SRURJU31AcmVkaXM6NjM3OS8xJwogICAgICAtICdPUEVOQUlfQUNDRVNTX1RPS0VOPSR7T1BFTkFJX0FDQ0VTU19UT0tFTn0nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21heWJlX3Bvc3RncmVzX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1tYXliZS1kYn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjgtYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzIC0tcmVxdWlyZXBhc3MgJHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUkVESVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JFRElTfScKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSBSRURJU19EQj0xCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSAnLS1wYXNzJwogICAgICAgIC0gJyR7U0VSVklDRV9QQVNTV09SRF9SRURJU30nCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogMwo=",
|
||||
"tags": [
|
||||
"finances",
|
||||
"wallets",
|
||||
"coins",
|
||||
"stocks",
|
||||
"investments",
|
||||
"open",
|
||||
"source"
|
||||
],
|
||||
"category": "productivity",
|
||||
"logo": "svgs/maybe.svg",
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"mealie": {
|
||||
"documentation": "https://docs.mealie.io/?utm_source=coolify.io",
|
||||
"slogan": "A recipe manager and meal planner.",
|
||||
|
|
@ -4548,6 +4530,22 @@
|
|||
"minversion": "0.0.0",
|
||||
"port": "3567"
|
||||
},
|
||||
"sure": {
|
||||
"documentation": "https://github.com/we-promise/sure?utm_source=coolify.io",
|
||||
"slogan": "An all-in-one personal finance platform.",
|
||||
"compose": "c2VydmljZXM6CiAgd2ViOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dlLXByb21pc2Uvc3VyZTowLjYuNycKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVJFXzMwMDAKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgdmFsa2V5OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLWFwcC1zdG9yYWdlOi9yYWlscy9zdG9yYWdlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICB3b3JrZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2UtcHJvbWlzZS9zdXJlOjAuNi43JwogICAgY29tbWFuZDogJ2J1bmRsZSBleGVjIHNpZGVraXEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBBUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fU1VSRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfQkFTRQogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotc3VyZX0nCiAgICAgIC0gJ1JFRElTX1VSTD1yZWRpczovL3ZhbGtleTo2Mzc5JwogICAgICAtIERCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPUlQ9NTQzMgogICAgICAtIFNFTEZfSE9TVEVEPXRydWUKICAgICAgLSBSQUlMU19GT1JDRV9TU0w9ZmFsc2UKICAgICAgLSBSQUlMU19BU1NVTUVfU1NMPWZhbHNlCiAgICAgIC0gJ09OQk9BUkRJTkdfU1RBVEU9JHtPTkJPQVJESU5HX1NUQVRFOi1vcGVufScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cmUtYXBwLXN0b3JhZ2U6L3JhaWxzL3N0b3JhZ2UnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHZhbGtleToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAxNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXN1cmV9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHZhbGtleToKICAgIGltYWdlOiAndmFsa2V5L3ZhbGtleTo4LWFscGluZScKICAgIGNvbW1hbmQ6ICd2YWxrZXktc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICdzdXJlLXZhbGtleTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAndmFsa2V5LWNsaSBwaW5nIHwgZ3JlcCBQT05HJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQogICAgICBzdGFydF9wZXJpb2Q6IDNzCg==",
|
||||
"tags": [
|
||||
"budgeting",
|
||||
"budget",
|
||||
"money",
|
||||
"expenses",
|
||||
"income"
|
||||
],
|
||||
"category": "finance",
|
||||
"logo": "svgs/sure.png",
|
||||
"minversion": "0.0.0",
|
||||
"port": "3000"
|
||||
},
|
||||
"swetrix": {
|
||||
"documentation": "https://docs.swetrix.com/selfhosting/how-to?utm_source=coolify.io",
|
||||
"slogan": "Privacy-friendly and cookieless European web analytics alternative to Google Analytics.",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
| need to change it using the "uses()" function to bind a different classes or traits.
|
||||
|
|
||||
*/
|
||||
uses(Tests\TestCase::class)->in('Feature');
|
||||
uses(Tests\TestCase::class)->in('Feature', 'v4/Feature', 'v4/Browser');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
46
tests/v4/Browser/LoginTest.php
Normal file
46
tests/v4/Browser/LoginTest.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0]);
|
||||
});
|
||||
|
||||
it('shows registration page when no users exist', function () {
|
||||
$page = visit('/login');
|
||||
|
||||
$page->assertSee('Root User Setup')
|
||||
->assertSee('Create Account');
|
||||
});
|
||||
|
||||
it('can login with valid credentials', function () {
|
||||
User::factory()->create([
|
||||
'id' => 0,
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
$page = visit('/login');
|
||||
|
||||
$page->assertSee('Login');
|
||||
});
|
||||
|
||||
it('fails login with invalid credentials', function () {
|
||||
User::factory()->create([
|
||||
'id' => 0,
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
$page = visit('/login');
|
||||
|
||||
$page->fill('email', 'random@email.com')
|
||||
->fill('password', 'wrongpassword123')
|
||||
->click('Login')
|
||||
->assertSee('These credentials do not match our records');
|
||||
});
|
||||
62
tests/v4/Browser/RegistrationTest.php
Normal file
62
tests/v4/Browser/RegistrationTest.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0]);
|
||||
});
|
||||
|
||||
it('shows registration page when no users exist', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
$page->assertSee('Root User Setup')
|
||||
->assertSee('Create Account');
|
||||
});
|
||||
|
||||
it('can register a new root user', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
$page->fill('name', 'Test User')
|
||||
->fill('email', 'root@example.com')
|
||||
->fill('password', 'Password1!@')
|
||||
->fill('password_confirmation', 'Password1!@')
|
||||
->click('Create Account')
|
||||
->assertPathIs('/onboarding');
|
||||
|
||||
expect(User::where('email', 'root@example.com')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('fails registration with mismatched passwords', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
$page->fill('name', 'Test User')
|
||||
->fill('email', 'root@example.com')
|
||||
->fill('password', 'Password1!@')
|
||||
->fill('password_confirmation', 'DifferentPass1!@')
|
||||
->click('Create Account')
|
||||
->assertSee('password');
|
||||
});
|
||||
|
||||
it('fails registration with weak password', function () {
|
||||
$page = visit('/register');
|
||||
|
||||
$page->fill('name', 'Test User')
|
||||
->fill('email', 'root@example.com')
|
||||
->fill('password', 'short')
|
||||
->fill('password_confirmation', 'short')
|
||||
->click('Create Account')
|
||||
->assertSee('password');
|
||||
});
|
||||
|
||||
it('shows login link when a user already exists', function () {
|
||||
User::factory()->create(['id' => 0]);
|
||||
|
||||
$page = visit('/register');
|
||||
|
||||
$page->assertSee('Already registered?')
|
||||
->assertDontSee('Root User Setup');
|
||||
});
|
||||
18
tests/v4/Feature/SqliteDatabaseTest.php
Normal file
18
tests/v4/Feature/SqliteDatabaseTest.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('uses sqlite for testing', function () {
|
||||
expect(DB::connection()->getDriverName())->toBe('sqlite');
|
||||
});
|
||||
|
||||
it('runs migrations successfully', function () {
|
||||
expect(Schema::hasTable('users'))->toBeTrue();
|
||||
expect(Schema::hasTable('teams'))->toBeTrue();
|
||||
expect(Schema::hasTable('servers'))->toBeTrue();
|
||||
expect(Schema::hasTable('applications'))->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in a new issue