test: migrate to SQLite :memory: and add Pest browser testing (#8364)
This commit is contained in:
commit
766355b9ac
22 changed files with 4034 additions and 96 deletions
|
|
@ -23,19 +23,28 @@ ## Documentation
|
||||||
|
|
||||||
Use `search-docs` for detailed Pest 4 patterns and 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.
|
## Running Tests
|
||||||
- Browser tests: `tests/Browser/` directory.
|
|
||||||
- Do NOT remove tests without approval - these are core application code.
|
|
||||||
|
|
||||||
### 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">
|
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -45,24 +54,10 @@ ### Basic Test Structure
|
||||||
|
|
||||||
</code-snippet>
|
</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
|
## Assertions
|
||||||
|
|
||||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
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 |
|
| Use | Instead of |
|
||||||
|-----|------------|
|
|-----|------------|
|
||||||
| `assertSuccessful()` | `assertStatus(200)` |
|
| `assertSuccessful()` | `assertStatus(200)` |
|
||||||
|
|
@ -75,7 +70,7 @@ ## Mocking
|
||||||
|
|
||||||
## Datasets
|
## Datasets
|
||||||
|
|
||||||
Use datasets for repetitive tests (validation rules, etc.):
|
Use datasets for repetitive tests:
|
||||||
|
|
||||||
<code-snippet name="Pest Dataset Example" lang="php">
|
<code-snippet name="Pest Dataset Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -88,73 +83,94 @@ ## Datasets
|
||||||
|
|
||||||
</code-snippet>
|
</code-snippet>
|
||||||
|
|
||||||
## Pest 4 Features
|
## Browser Testing (Pest Browser Plugin + Playwright)
|
||||||
|
|
||||||
| Feature | Purpose |
|
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 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 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/`.
|
### Browser Test Template
|
||||||
- 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.
|
|
||||||
|
|
||||||
<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 () {
|
use App\Models\InstanceSettings;
|
||||||
Notification::fake();
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
$page = visit('/sign-in');
|
beforeEach(function () {
|
||||||
|
InstanceSettings::create(['id' => 0]);
|
||||||
$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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can visit the page', function () {
|
||||||
|
$page = visit('/login');
|
||||||
|
|
||||||
|
$page->assertSee('Login');
|
||||||
|
});
|
||||||
</code-snippet>
|
</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">
|
$page = visit('/login');
|
||||||
|
|
||||||
$pages = visit(['/', '/about', '/contact']);
|
|
||||||
|
|
||||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
|
||||||
|
|
||||||
|
$page->fill('email', 'random@email.com')
|
||||||
|
->fill('password', 'wrongpassword123')
|
||||||
|
->click('Login')
|
||||||
|
->assertSee('These credentials do not match our records');
|
||||||
|
});
|
||||||
</code-snippet>
|
</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">
|
<code-snippet name="Architecture Test Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -171,4 +187,7 @@ ## Common Pitfalls
|
||||||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||||
- Forgetting datasets for repetitive validation tests
|
- Forgetting datasets for repetitive validation tests
|
||||||
- Deleting tests without approval
|
- Deleting tests without approval
|
||||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
- 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
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -38,3 +38,5 @@ docker/coolify-realtime/node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
/.workspaces
|
/.workspaces
|
||||||
|
tests/Browser/Screenshots
|
||||||
|
tests/v4/Browser/Screenshots
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -167,6 +167,10 @@ function currentTeam()
|
||||||
|
|
||||||
function showBoarding(): bool
|
function showBoarding(): bool
|
||||||
{
|
{
|
||||||
|
if (isDev()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Auth::user()?->isMember()) {
|
if (Auth::user()?->isMember()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
"mockery/mockery": "^1.6.12",
|
"mockery/mockery": "^1.6.12",
|
||||||
"nunomaduro/collision": "^8.8.3",
|
"nunomaduro/collision": "^8.8.3",
|
||||||
"pestphp/pest": "^4.3.2",
|
"pestphp/pest": "^4.3.2",
|
||||||
|
"pestphp/pest-plugin-browser": "^4.2",
|
||||||
"phpstan/phpstan": "^2.1.38",
|
"phpstan/phpstan": "^2.1.38",
|
||||||
"rector/rector": "^2.3.5",
|
"rector/rector": "^2.3.5",
|
||||||
"serversideup/spin": "^3.1.1",
|
"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' => [
|
'testing' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'sqlite',
|
||||||
'url' => env('DATABASE_TEST_URL'),
|
'database' => ':memory:',
|
||||||
'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',
|
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'foreign_key_constraints' => true,
|
||||||
'search_path' => 'public',
|
|
||||||
'sslmode' => 'prefer',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ class AddIndexToActivityLog extends Migration
|
||||||
{
|
{
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb');
|
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)');
|
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()
|
public function down()
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
||||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@
|
||||||
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
||||||
if (! $this->indexExists($indexName)) {
|
if (! $this->indexExists($indexName)) {
|
||||||
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
||||||
|
|
@ -32,6 +36,10 @@ public function up(): void
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->indexes as [, , $indexName]) {
|
foreach ($this->indexes as [, , $indexName]) {
|
||||||
DB::statement("DROP INDEX IF EXISTS \"{$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:
|
volumes:
|
||||||
- .:/var/www/html/:cached
|
- .:/var/www/html/:cached
|
||||||
- dev_backups_data:/var/www/html/storage/app/backups
|
- dev_backups_data:/var/www/html/storage/app/backups
|
||||||
|
networks:
|
||||||
|
- coolify
|
||||||
postgres:
|
postgres:
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
47
package-lock.json
generated
47
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
||||||
"@tailwindcss/typography": "0.5.16",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "5.5.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"ioredis": "5.6.1"
|
"ioredis": "5.6.1",
|
||||||
|
"playwright": "^1.58.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
|
|
@ -2338,6 +2339,50 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"@tailwindcss/typography": "0.5.16",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "5.5.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">
|
<testsuite name="Feature">
|
||||||
<directory suffix="Test.php">./tests/Feature</directory>
|
<directory suffix="Test.php">./tests/Feature</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
<testsuite name="v4">
|
||||||
|
<directory suffix="Test.php">./tests/v4</directory>
|
||||||
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<coverage/>
|
<coverage/>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array" force="true"/>
|
||||||
<env name="DB_CONNECTION" value="testing"/>
|
<env name="DB_CONNECTION" value="testing" force="true"/>
|
||||||
<env name="DB_TEST_DATABASE" value="coolify"/>
|
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
</php>
|
</php>
|
||||||
<source>
|
<source>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
function getOldOrLocal($key, $localValue)
|
if (! function_exists('getOldOrLocal')) {
|
||||||
{
|
function getOldOrLocal($key, $localValue)
|
||||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
{
|
||||||
|
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = getOldOrLocal('name', 'test3 normal user');
|
$name = getOldOrLocal('name', 'test3 normal user');
|
||||||
|
|
|
||||||
2
tests/Browser/screenshots/.gitignore
vendored
2
tests/Browser/screenshots/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
| need to change it using the "uses()" function to bind a different classes or traits.
|
| 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');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
162
tests/v4/Browser/DashboardTest.php
Normal file
162
tests/v4/Browser/DashboardTest.php
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Enums\ProxyStatus;
|
||||||
|
use App\Enums\ProxyTypes;
|
||||||
|
use App\Models\InstanceSettings;
|
||||||
|
use App\Models\PrivateKey;
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
InstanceSettings::create(['id' => 0]);
|
||||||
|
|
||||||
|
$this->user = User::factory()->create([
|
||||||
|
'id' => 0,
|
||||||
|
'name' => 'Root User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
PrivateKey::create([
|
||||||
|
'id' => 1,
|
||||||
|
'uuid' => 'ssh-test',
|
||||||
|
'team_id' => 0,
|
||||||
|
'name' => 'Test Key',
|
||||||
|
'description' => 'Test SSH key',
|
||||||
|
'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||||
|
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||||
|
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||||
|
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Server::create([
|
||||||
|
'id' => 0,
|
||||||
|
'uuid' => 'localhost',
|
||||||
|
'name' => 'localhost',
|
||||||
|
'description' => 'This is a test docker container in development mode',
|
||||||
|
'ip' => 'coolify-testing-host',
|
||||||
|
'team_id' => 0,
|
||||||
|
'private_key_id' => 1,
|
||||||
|
'proxy' => [
|
||||||
|
'type' => ProxyTypes::TRAEFIK->value,
|
||||||
|
'status' => ProxyStatus::EXITED->value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Server::create([
|
||||||
|
'uuid' => 'production-1',
|
||||||
|
'name' => 'production-web',
|
||||||
|
'description' => 'Production web server cluster',
|
||||||
|
'ip' => '10.0.0.1',
|
||||||
|
'team_id' => 0,
|
||||||
|
'private_key_id' => 1,
|
||||||
|
'proxy' => [
|
||||||
|
'type' => ProxyTypes::TRAEFIK->value,
|
||||||
|
'status' => ProxyStatus::EXITED->value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Server::create([
|
||||||
|
'uuid' => 'staging-1',
|
||||||
|
'name' => 'staging-server',
|
||||||
|
'description' => 'Staging environment server',
|
||||||
|
'ip' => '10.0.0.2',
|
||||||
|
'team_id' => 0,
|
||||||
|
'private_key_id' => 1,
|
||||||
|
'proxy' => [
|
||||||
|
'type' => ProxyTypes::TRAEFIK->value,
|
||||||
|
'status' => ProxyStatus::EXITED->value,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Project::create([
|
||||||
|
'uuid' => 'project-1',
|
||||||
|
'name' => 'My first project',
|
||||||
|
'description' => 'This is a test project in development',
|
||||||
|
'team_id' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Project::create([
|
||||||
|
'uuid' => 'project-2',
|
||||||
|
'name' => 'Production API',
|
||||||
|
'description' => 'Backend services for production',
|
||||||
|
'team_id' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Project::create([
|
||||||
|
'uuid' => 'project-3',
|
||||||
|
'name' => 'Staging Environment',
|
||||||
|
'description' => 'Staging and QA testing',
|
||||||
|
'team_id' => 0,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function loginAndSkipOnboarding(): mixed
|
||||||
|
{
|
||||||
|
return visit('/login')
|
||||||
|
->fill('email', 'test@example.com')
|
||||||
|
->fill('password', 'password')
|
||||||
|
->click('Login')
|
||||||
|
->click('Skip Setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('redirects to login when not authenticated', function () {
|
||||||
|
$page = visit('/');
|
||||||
|
|
||||||
|
$page->assertPathIs('/login')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows onboarding after first login', function () {
|
||||||
|
$page = visit('/login');
|
||||||
|
|
||||||
|
$page->fill('email', 'test@example.com')
|
||||||
|
->fill('password', 'password')
|
||||||
|
->click('Login')
|
||||||
|
->assertSee('Welcome to Coolify')
|
||||||
|
->assertSee("Let's go!")
|
||||||
|
->assertSee('Skip Setup')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows dashboard after skipping onboarding', function () {
|
||||||
|
$page = loginAndSkipOnboarding();
|
||||||
|
|
||||||
|
$page->assertSee('Dashboard')
|
||||||
|
->assertSee('Your self-hosted infrastructure.')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows all projects on dashboard', function () {
|
||||||
|
$page = loginAndSkipOnboarding();
|
||||||
|
|
||||||
|
$page->assertSee('Projects')
|
||||||
|
->assertSee('My first project')
|
||||||
|
->assertSee('This is a test project in development')
|
||||||
|
->assertSee('Production API')
|
||||||
|
->assertSee('Backend services for production')
|
||||||
|
->assertSee('Staging Environment')
|
||||||
|
->assertSee('Staging and QA testing')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows servers on dashboard', function () {
|
||||||
|
$page = loginAndSkipOnboarding();
|
||||||
|
|
||||||
|
$page->assertSee('Servers')
|
||||||
|
->assertSee('localhost')
|
||||||
|
->assertSee('This is a test docker container in development mode')
|
||||||
|
->assertSee('production-web')
|
||||||
|
->assertSee('Production web server cluster')
|
||||||
|
->assertSee('staging-server')
|
||||||
|
->assertSee('Staging environment server')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
52
tests/v4/Browser/LoginTest.php
Normal file
52
tests/v4/Browser/LoginTest.php
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can login with valid credentials', function () {
|
||||||
|
User::factory()->create([
|
||||||
|
'id' => 0,
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$page = visit('/login');
|
||||||
|
|
||||||
|
$page->fill('email', 'test@example.com')
|
||||||
|
->fill('password', 'password')
|
||||||
|
->click('Login')
|
||||||
|
->assertSee('Welcome to Coolify')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
67
tests/v4/Browser/RegistrationTest.php
Normal file
67
tests/v4/Browser/RegistrationTest.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
||||||
|
->screenshot();
|
||||||
|
|
||||||
|
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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
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')
|
||||||
|
->screenshot();
|
||||||
|
});
|
||||||
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