diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 288d00b..472b90c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,17 +12,18 @@ Install the required PHP and Node packages, then configure a local PostgreSQL database in `.env`: ```bash -composer install -npm ci +# Fresh installations only; keep an existing .env. cp .env.example .env -php artisan key:generate -php artisan migrate +# Configure PostgreSQL credentials in .env before setup. +composer run setup composer run dev ``` -`composer run dev` starts the Laravel server, the queue listener, and the Vite dev -server together. The app supports PHP 8.3+, while the current Pest 5 test suite -needs PHP 8.4. See [local development](docs/getting-started/local.md) for the +`composer run setup` installs locked dependencies, preserves an existing app key, +migrates, creates `ADMIN_EMAIL` if configured, and builds the frontend. +`composer run dev` starts the Laravel server, Vite, logs when supported, all +content queues, and the scheduler together. The app supports PHP 8.3+, while +the current Pest 5 test suite needs PHP 8.4. See [local development](docs/getting-started/local.md) for the PostgreSQL setup used by CI. The app does not provide a local Docker Compose stack. diff --git a/README.md b/README.md index 4ff376b..3eb49e6 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,251 @@ player for video decks. See [the architecture overview](docs/architecture/overview.md), [the API guide](docs/guides/api.md), and [the PostSyncer guide](docs/guides/postsyncer.md). -## Run locally +## Setup and usage -You'll need PHP 8.3+, Composer, Node.js, npm, and PostgreSQL. The test suite -currently needs PHP 8.4 because Pest 5 requires it. Follow [local -setup](docs/getting-started/local.md) for installation and database setup. +Follow these steps to go from a fresh checkout to your first published post. +The dashboard works without external integrations; AI generation, Telegram, +Google Drive, and publishing each need their own configuration. + +### 1. Install and start locally + +You need PHP 8.3+ with `pdo_pgsql`, `intl`, `zip`, `bcmath`, and `gd`, Composer, +Node.js, npm, and PostgreSQL. The current Pest 5 test suite needs PHP 8.4. +CI uses PostgreSQL 17 and Node 26. + +From the project directory: ```bash -composer install -npm ci +# Fresh installations only: keep your existing .env if you already have one. cp .env.example .env -php artisan key:generate -php artisan migrate +``` + +Create a PostgreSQL database and database user, then edit `.env` to match them: + +```dotenv +APP_URL=http://127.0.0.1:8000 +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=content_machine +DB_USERNAME=your_database_user +DB_PASSWORD=your_database_password +QUEUE_CONNECTION=database +``` + +Then initialize the application: + +```bash +composer run setup composer run dev ``` +`composer run setup` installs PHP and Node dependencies from their lockfiles, +creates `.env` if missing, generates `APP_KEY` only when missing, runs migrations, +creates `ADMIN_EMAIL` if configured, and builds the frontend. Existing encrypted +integration credentials depend on preserving the same application key. +Open the URL printed by Laravel, usually , and keep the +development process running. +See [local development](docs/getting-started/local.md) for more details. + +### 2. Create your first account + +Choose one of these methods. Both create an account with its own team and +default workspace, which the dashboard requires. + +**Register in the browser:** leave `DISABLE_REGISTRATION=false` in `.env`, open +`/register`, and create your account. Successful registration takes you to +`/dashboard`. + +**Create an owner account from the terminal:** add these values to `.env`: + +```dotenv +ADMIN_EMAIL=you@example.com +ADMIN_NAME="Your Name" +``` + +In a separate terminal, from the project directory, run: + +```bash +php artisan config:clear +php artisan cm:ensure-admin +``` + +The command prints a generated password once. Sign in at `/login`, open +`/dashboard`, then change the password under account security. The account owns +its team; there is no separate global admin flag to enable. + +If the command says the account already exists, it leaves that account +unchanged. It does not reset its password or repair its workspace. + +After creating your account, you can close public registration: + +```dotenv +DISABLE_REGISTRATION=true +``` + +Run `php artisan config:clear` after changing that setting. + +Do not use `php artisan db:seed` as the first-account setup. The current seeder +creates `test@example.com` without a team or workspace, which causes a dashboard +404. See [recovering an account with no workspace](docs/getting-started/local.md#dashboard-404-no-current-workspace) +if you already used that account. + +### 3. Start development services + +`composer run dev` starts the Laravel server, Vite, logs when supported, the +default queue worker, the scratchpad worker, the PostSyncer worker, and the +scheduler together. Keep it running while using the app; no extra worker +terminals are needed for local development. Restart it after changing worker +code or configuration. + +The scratchpad worker handles Telegram media, transcription, and AI post +generation. The PostSyncer worker handles post and video publishing. The +scheduler recovers pending work and refreshes published status. The database +queue is enough to start; Redis is not required. + +For production, use the separate supervised processes in the +[production setup guide](docs/getting-started/production.md), rather than the +development server. + +### 4. Configure integrations + +Integration settings belong to the current workspace. Configure them while +signed in as its owner. + +| Integration | Setup | Enables | +|---|---|---| +| AI Providers | Open `/settings/ai-providers`. Add an enabled provider with its API key, then add a model under Models > Default. Add a suitable model under Vision for photo input. | AI post drafts, summaries, and triage suggestions. Saving a key alone does not add a model to the generation chain. | +| Telegram | Open `/settings/telegram`. Connect a bot token, get a link code, and send `/link CODE` to that bot from your Telegram account. | Capture and the draft > approve > publish workflow. Each member links their own Telegram account. | +| PostSyncer | Connect your social accounts in PostSyncer. In `/settings/postsyncer`, save its API key. In `/settings/postsyncer/workspaces`, select language workspaces and activate the desired accounts. Enable publishing when ready. | Scheduling and delivery to the configured social accounts. | +| Google Drive (optional) | Configure Google OAuth in `.env`, then connect through `/settings/google-drive` and select an export folder. | Browse and select finished video files and covers from Drive. Public Drive links can also be pasted directly. | + +AI providers support OpenAI and Anthropic request formats, including compatible +custom base URLs. Voice transcription additionally needs a compatible +`/audio/transcriptions` endpoint; a text or vision model alone is insufficient. + +Telegram needs a publicly reachable HTTPS webhook. A bot cannot deliver updates +to `127.0.0.1`; for local testing, expose the app through an HTTPS tunnel and set +`APP_URL` to that public URL before connecting the bot. PostSyncer also needs to +fetch the app's signed attachment URLs, so local uploaded-media publishing needs +public reachability too. Clear cached configuration after changing `APP_URL`. + +See the [Telegram guide](docs/guides/telegram-bot.md) and +[PostSyncer/Google Drive guide](docs/guides/postsyncer.md) for detailed setup. + +### 5. Generate and publish your first post + +With an AI model, linked Telegram account, and PostSyncer configured, send this +to your bot: + +```text +/post English: Our website maintenance service includes weekly backups, security updates, and monthly reports. +``` + +The bot saves the source, generates a draft with Facebook and Instagram +captions, and sends a preview. Review the draft in Content Machine and edit +its content or captions before approving it. Use the actual ID from the preview: + +```text +/approve P-123 +``` + +To publish immediately: + +```text +/post_now P-123 +``` + +To schedule, use a future date and time in the workspace timezone instead: + +```text +/schedule P-123 YYYY-MM-DD HH:MM +``` + +Replace the date/time placeholders with actual values. Scheduling defaults to +Asia/Dhaka for a workspace using the default timezone. Generation never +publishes automatically; generated drafts require approval first. + +For photo-based content, send `/post`, then a photo, or put `/post` at the start +of the photo caption. For spoken content, send `/post`, then a voice note or +audio file. Request English or Bangla explicitly; configured language workspaces +can also drive bilingual generation. Use `/cancel` to cancel an active request. + +You can also open the post in the dashboard and select **Schedule** or +**Publish now**. Available platforms depend on the post's platforms, active +PostSyncer accounts, and post-type rules. Instagram needs suitable media; +text-only publishing is available only where the configured platform rules +allow it. Every caption language needs its own configured PostSyncer workspace. + +### 6. Work through the dashboard or publish videos + +The dashboard workflow is **Scratch Pad > post/video idea > draft > ready > +scheduled > posted/published**. Capture a note or link in Scratch Pad, optionally +use **Suggest with AI**, file it as an idea, and promote the idea to a draft. +Promotion copies the idea's title and body into a draft shell; it does not write +a complete AI post. Use Telegram `/post` for built-in draft generation, or write +content yourself and edit it in the dashboard. + +For videos, use Content Machine to manage ideas, scripts, series, presentation +decks, and captions. Record and edit the final video with your production tools, +then upload it to Google Drive. On the video page, set **Video Drive URL** and +optionally **Cover Drive URL**, select platforms, and schedule or publish. +These must be public file links, not private files or folders. The Drive picker +can grant Anyone-with-the-link access when selecting publishing media. Content +Machine does not generate a finished video file from a prompt. + +### 7. Connect external tools + +Open **Team > API access**, create a workspace token with the abilities your +client needs, and save the token when shown. Use it as a bearer token for the +REST API at `/api/v1` or the Streamable HTTP MCP endpoint at `/mcp`. + +The REST API can create and update content, attach media, approve posts, and +queue publishing. MCP tools can capture notes, triage ideas, update existing +posts/videos, and publish them. An external writing tool can generate content +and save drafts through the REST API for review. See the +[API guide](docs/guides/api.md) for endpoints, permissions, and client setup. +For API/MCP publishing, omitting `when` means publish immediately; always pass +`when` when scheduling. + +### Troubleshooting + +| Symptom | What to check | +|---|---| +| Login succeeds but `/dashboard` returns 404 | The account needs an active team and workspace. See the [account recovery steps](docs/getting-started/local.md#dashboard-404-no-current-workspace). | +| Admin command skips the account | `ADMIN_EMAIL` already exists. Use its existing password or password recovery; the command does not reset or repair it. | +| `/register` returns 403 | Set `DISABLE_REGISTRATION=false` and clear cached configuration, or create an owner through `cm:ensure-admin`. | +| AI draft generation fails | Enable a provider and add a model to the Default or Vision chain, as appropriate. Check that the scratchpad process in `composer run dev` is running. | +| Telegram does not respond | Check public HTTPS reachability, bot connection, your account link, and both the default and scratchpad workers. | +| Schedule/Publish is unavailable | Check the PostSyncer key, language workspaces, active accounts, publish toggle, approval, platform rules, and required media. | +| A publish remains queued | Keep `composer run dev` running locally; in production, supervise `composer run worker:publish`. | +| A publish has an uncertain outcome | Follow [PostSyncer recovery](docs/guides/postsyncer.md) before retrying; do not create a second copy blindly. | + +## Production setup + +Configure the production environment and database first, then run +`composer run setup:production`. This installs PHP dependencies without dev +packages, requires an existing `APP_KEY`, builds the frontend, runs the existing +`cm:deploy` deployment steps, and caches the application. It preserves the +application key and creates `ADMIN_EMAIL` only when that account does not exist. + +Use PHP-FPM with a web server and supervise the queue workers and scheduler. +See [production setup](docs/getting-started/production.md) for the environment, +first key, process commands, uploads, and repeat deployments. The existing +Railway configuration continues to use its Docker build and `cm:deploy` hook. + +## Composer commands + +| Command | Purpose | +|---|---| +| `composer run setup` | Install and initialize a local checkout without rotating an existing key. | +| `composer run dev` | Start the complete local development workflow. | +| `composer run setup:production` | Install and prepare a production release with a preconfigured environment and key. | +| `composer run worker:default` | Run the default queue worker. | +| `composer run worker:scratchpad` | Run media, transcription, and generation jobs. | +| `composer run worker:publish` | Run PostSyncer publishing jobs. | +| `composer run scheduler` | Run the Laravel scheduler. | + ## Contributing See [`CONTRIBUTING.md`](CONTRIBUTING.md) for project conventions and checks. diff --git a/app/Console/Commands/EnsureApplicationKeyCommand.php b/app/Console/Commands/EnsureApplicationKeyCommand.php new file mode 100644 index 0000000..f9f92c7 --- /dev/null +++ b/app/Console/Commands/EnsureApplicationKeyCommand.php @@ -0,0 +1,33 @@ +components->info('Application key already configured, keeping it.'); + + return self::SUCCESS; + } + + if ($this->option('require-existing') || app()->environment('production', 'prod')) { + $this->components->error('Set APP_KEY before production setup. Generate it once on the first installation and preserve it across deployments.'); + + return self::FAILURE; + } + + if ($this->call('key:generate') !== self::SUCCESS || blank(config('app.key'))) { + return self::FAILURE; + } + + return self::SUCCESS; + } +} diff --git a/composer.json b/composer.json index 50cc796..58d5c76 100644 --- a/composer.json +++ b/composer.json @@ -44,17 +44,44 @@ }, "scripts": { "setup": [ - "composer install", + "@composer install --no-interaction", "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"", - "@php artisan key:generate", - "@php artisan migrate --force", - "npm install", + "@php artisan config:clear", + "@php artisan cm:ensure-key", + "@php artisan migrate", + "@php artisan cm:ensure-admin", + "npm ci", "npm run build" ], + "setup:production": [ + "@composer install --no-dev --optimize-autoloader --no-interaction", + "@php artisan config:clear", + "@php artisan cm:ensure-key --require-existing", + "npm ci --include=dev", + "npm run build", + "@php artisan cm:deploy", + "@php artisan optimize" + ], "dev": [ "Composer\\Config::disableProcessTimeout", "@php artisan dev" ], + "worker:default": [ + "Composer\\Config::disableProcessTimeout", + "@php artisan queue:work --queue=default --tries=3 --timeout=900" + ], + "worker:scratchpad": [ + "Composer\\Config::disableProcessTimeout", + "@php artisan queue:work --queue=scratchpad --tries=3 --timeout=900" + ], + "worker:publish": [ + "Composer\\Config::disableProcessTimeout", + "@php artisan queue:work postsyncer --tries=3 --timeout=900" + ], + "scheduler": [ + "Composer\\Config::disableProcessTimeout", + "@php artisan schedule:work" + ], "lint": [ "pint --parallel" ], @@ -88,8 +115,7 @@ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ - "@php artisan key:generate --ansi", - "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"", + "@php artisan cm:ensure-key --ansi", "@php artisan migrate --graceful --ansi" ], "pre-package-uninstall": [ @@ -114,5 +140,14 @@ } }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "scripts-descriptions": { + "setup": "Install local dependencies, preserve or generate APP_KEY, migrate, create ADMIN_EMAIL if configured, and build assets.", + "setup:production": "Install production PHP dependencies, require an existing APP_KEY, build assets, deploy, and cache the app.", + "dev": "Start the web server, Vite, logs, all content queues, and scheduler for local development.", + "worker:default": "Run the default queue worker under a production process supervisor.", + "worker:scratchpad": "Run Telegram media, transcription, and generation jobs where uploads are stored.", + "worker:publish": "Run PostSyncer publishing jobs where uploads are stored.", + "scheduler": "Run the Laravel scheduler under a process supervisor." + } } diff --git a/docs/README.md b/docs/README.md index 8e5fe70..1c866d2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,10 @@ # Documentation +- [Setup and usage overview](../README.md#setup-and-usage) +- [First account and login](../README.md#2-create-your-first-account) +- [Dashboard 404: account recovery](getting-started/local.md#dashboard-404-no-current-workspace) - [Getting started: local development](getting-started/local.md) +- [Production setup](getting-started/production.md) - [Architecture overview](architecture/overview.md) - [Posts and videos](architecture/posts-videos-api.md) - [Guides](guides/) diff --git a/docs/getting-started/local.md b/docs/getting-started/local.md index 470b04f..901aae7 100644 --- a/docs/getting-started/local.md +++ b/docs/getting-started/local.md @@ -12,21 +12,85 @@ The application declares PHP 8.3 support. The test toolchain currently needs PHP ## Start the app Create a local PostgreSQL database and user. `.env.example` uses -`content_machine` on `127.0.0.1:5432` with user `root` and no password. Adjust -those values for your local server. +`content_machine` on `127.0.0.1:5432`. Set the database username and password +to a PostgreSQL role that exists on your local server. ```bash -composer install -npm ci +# Fresh installations only; keep an existing .env. cp .env.example .env # Edit .env with your local database host, port, name, username, and password. -php artisan key:generate -php artisan migrate +composer run setup composer run dev ``` -`composer run dev` starts the Laravel server, queue listener, and Vite dev -server. Open the URL printed by Laravel, usually `http://localhost:8000`. +`composer run setup` installs locked dependencies, prepares a missing application +key without replacing an existing one, migrates the database, creates +`ADMIN_EMAIL` if configured, and builds the frontend. + +`composer run dev` starts the Laravel server, Vite, logs when supported, the +default queue worker, scratchpad and PostSyncer workers, and scheduler. Open +the URL printed by Laravel, usually `http://localhost:8000`. + +## First login and background workers + +After migration, create an account at `/register`, or use `ADMIN_EMAIL`, +`ADMIN_NAME`, and `php artisan cm:ensure-admin` to create an owner account with +a generated password. Both supported setup paths create a team and default +workspace. The admin command skips existing accounts without repairing them or +resetting their passwords. + +Follow the [README account setup](../../README.md#2-create-your-first-account) +for the exact commands and the +[development services](../../README.md#3-start-development-services) for the +processes started together by `composer run dev`. Use the separate supervised +worker commands from the [production guide](production.md) on a server. + +## Dashboard 404: No current workspace + +If login succeeds but `/dashboard` returns `404 No current workspace`, the +account has no resolvable active team/workspace. The current `DatabaseSeeder` +creates `test@example.com` without those records, so `php artisan db:seed` is +not a substitute for registration or the admin setup command. + +For a fresh installation, use a new email with one of the supported setup paths +above. To keep an existing local account, repair its team/workspace through the +application's team-creation action. Run: + +```bash +php artisan tinker +``` + +Paste the following block, replacing the email if needed. It preserves the +password and reuses an existing membership and workspace when possible: + +```php +Illuminate\Support\Facades\DB::transaction(function () { + $user = App\Models\User::where('email', 'test@example.com') + ->lockForUpdate() + ->firstOrFail(); + + $team = $user->teams()->whereKey($user->current_team_id)->first() + ?? $user->teams()->oldest('teams.id')->first(); + + if ($team === null) { + $team = app(App\Actions\Teams\CreateTeamAction::class)->handle( + $user, + App\Data\Teams\CreateTeamData::fromOwner($user), + ); + } + + if (! $team->workspaces()->exists()) { + $team->workspaces()->create(['name' => 'Default', 'slug' => 'default']); + } + + $user->switchTeam($team); +}); +``` + +Exit Tinker and refresh `/dashboard`. Creating a missing team through +`CreateTeamAction` also creates the owner membership and default workspace. +Running `cm:ensure-admin` for the same existing email will not perform this +repair. Change any default development password before making the app public. ## Run tests against a disposable PostgreSQL database diff --git a/docs/getting-started/production.md b/docs/getting-started/production.md new file mode 100644 index 0000000..e6f75a0 --- /dev/null +++ b/docs/getting-started/production.md @@ -0,0 +1,140 @@ +# Production setup + +This guide covers a single-server installation. The repository also includes +Railway web and worker definitions under `deploy/`; those retain their Docker +build and `php artisan cm:deploy` pre-deploy hook. + +## Requirements + +- PHP 8.3+ with the extensions listed in the [local setup guide](local.md). +- Composer, Node.js, npm, and PostgreSQL. CI uses PHP 8.4, Node 26, and PostgreSQL 17. +- A web server with PHP-FPM, HTTPS, and the document root set to `public/`. +- A process supervisor such as Supervisor or systemd for workers and scheduling. +- Writable `storage/` and `bootstrap/cache/` directories for the runtime user. +- Persistent storage for `storage/app/uploads`, including across releases. + +## Configure the first installation + +Create the PostgreSQL database and role. On a fresh checkout, copy `.env.example` +to `.env` and configure it before running setup. Keep an existing environment +file on later releases. At minimum, set: + +```dotenv +APP_ENV=production +APP_DEBUG=false +APP_URL=https://content.example.com +DISABLE_REGISTRATION=true +ADMIN_EMAIL=you@example.com +ADMIN_NAME="Your Name" + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=content_machine +DB_USERNAME=content_machine +DB_PASSWORD=your_database_password + +QUEUE_CONNECTION=database +POSTSYNCER_QUEUE_RETRY_AFTER=960 +CACHE_STORE=database +SESSION_DRIVER=database +``` + +`ADMIN_EMAIL` creates an account with its own owner team and default workspace. +The command prints its generated password once; use it at `/login`, then change +it under account security. An existing account is left unchanged. Do not run +the development database seeder as an account-provisioning step. + +Configure a real mail transport if you need password recovery or invitation +email delivery. The example environment logs mail instead of delivering it. + +## Set the application key once + +The production script requires `APP_KEY` to be provisioned beforehand. For the +first installation only, install the PHP dependencies and generate the key: + +```bash +composer install --no-dev --optimize-autoloader --no-interaction +php artisan config:clear +php artisan key:generate --force +``` + +Store that key securely and reuse it on every release and every process. For +environment-variable deployments, provision the key through the host's secret +settings. Do not regenerate it on later deployments; saved provider, Telegram, +and PostSyncer credentials are encrypted with it. + +## Prepare the release + +With the environment and application key configured, run: + +```bash +composer run setup:production +``` + +This command performs these steps and stops if any step fails: + +1. Install locked PHP dependencies without development packages. +2. Clear cached configuration and require an existing application key. +3. Install locked Node dependencies, including the build tools, and build assets. +4. Run `cm:deploy` for migrations, upload-directory preparation, existing data + maintenance, and optional first owner creation. +5. Cache application configuration, routes, and views with `artisan optimize`. + +The script does not start the web server or supervise background processes. +Node development dependencies are needed to build Vite assets; they do not +change the `--no-dev` PHP installation. + +The existing deployment command defers Telegram generation cutover migrations +in production unless the cutover is explicitly configured. Existing Telegram +deployments must follow the fleet-drain and preflight requirements in +`telegram:cutover-preflight --help` and `.env.example`. Preserve these gates; +do not replace `cm:deploy` with a blanket forced migration command. + +## Supervise the runtime processes + +Serve the application through your web server and PHP-FPM. Configure one +supervised process for each command below, using the project root as its working +directory and the same environment as the web application: + +```bash +composer run worker:default +composer run worker:scratchpad +composer run worker:publish +composer run scheduler +``` + +These are four separate long-running processes, not a sequence to run in one +terminal. Configure automatic restarts and allow at least 960 seconds for +workers to finish during shutdown. Workers use a 900-second job timeout, and +the dedicated PostSyncer queue visibility window must exceed that timeout. + +The scratchpad and publishing workers must be able to read the same persistent +uploads as the web process. For the existing Railway layout, these workers run +inside the web container, while the default worker and scheduler run in the +worker service. See the [PostSyncer deployment guide](../guides/postsyncer.md#queue-and-worker-railway). + +Run a single scheduler per deployment. If your host already schedules +`php artisan schedule:run` every minute, use that instead of `composer run scheduler`. +Use `composer run dev` only for local development. + +## Configure integrations and publish + +Sign in, then follow the [integration setup](../../README.md#4-configure-integrations) +and [first-post workflow](../../README.md#5-generate-and-publish-your-first-post). +Telegram requires a public HTTPS webhook, and PostSyncer must be able to fetch +signed media URLs generated from `APP_URL`. Google Drive publishing files must +be publicly fetchable. + +## Later releases + +Back up the database and preserve the environment, application key, and uploads. +Prepare the new release with `composer run setup:production`, then activate it +using your host's deployment procedure. Restart PHP-FPM as appropriate and +restart all supervised workers and the scheduler so they load the new code and +configuration. Avoid overlapping schedulers during the switch. + +For deployments using a separate build stage, build dependencies and assets +there and run the existing `php artisan cm:deploy` release hook against the +production database. The provided Railway Docker configuration already follows +that model; it does not need the combined single-server Composer setup script. diff --git a/routes/console.php b/routes/console.php index b5c511d..b26d458 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,9 +1,16 @@ comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); diff --git a/tests/Feature/Console/DevelopmentProcessesTest.php b/tests/Feature/Console/DevelopmentProcessesTest.php new file mode 100644 index 0000000..d4f8cb2 --- /dev/null +++ b/tests/Feature/Console/DevelopmentProcessesTest.php @@ -0,0 +1,12 @@ +pluck('command', 'name'); + + expect($commands->get('queue'))->toBe('composer run worker:default'); + expect($commands->get('scratchpad'))->toBe('composer run worker:scratchpad'); + expect($commands->get('publishing'))->toBe('composer run worker:publish'); + expect($commands->get('scheduler'))->toBe('composer run scheduler'); +}); diff --git a/tests/Feature/Console/EnsureApplicationKeyCommandTest.php b/tests/Feature/Console/EnsureApplicationKeyCommandTest.php new file mode 100644 index 0000000..8ae6428 --- /dev/null +++ b/tests/Feature/Console/EnsureApplicationKeyCommandTest.php @@ -0,0 +1,56 @@ + 'existing-key']); + $original = File::get(app()->environmentFilePath()); + + $this->artisan('cm:ensure-key')->assertSuccessful(); + + expect(config('app.key'))->toBe('existing-key'); + expect(File::get(app()->environmentFilePath()))->toBe($original); +}); + +it('requires an existing key for production setup', function () { + config(['app.key' => null]); + + $this->artisan('cm:ensure-key --require-existing')->assertFailed(); + + expect(config('app.key'))->toBeNull(); +}); + +it('does not generate a key in a production environment', function () { + config(['app.key' => null]); + app()->instance('env', 'production'); + + $this->artisan('cm:ensure-key')->assertFailed(); + + expect(config('app.key'))->toBeNull(); +}); + +it('generates a missing local key once without rotating it on a second run', function () { + $directory = sys_get_temp_dir().'/cm-key-'.bin2hex(random_bytes(8)); + File::makeDirectory($directory); + File::put($directory.'/.env', "APP_KEY=\n"); + $originalPath = app()->environmentPath(); + app()->useEnvironmentPath($directory); + config(['app.key' => null]); + + try { + $this->artisan('cm:ensure-key')->assertSuccessful(); + $key = config('app.key'); + $contents = File::get($directory.'/.env'); + + expect($key)->toStartWith('base64:'); + expect($contents)->toContain('APP_KEY='.$key); + + $this->artisan('cm:ensure-key')->assertSuccessful(); + + expect(config('app.key'))->toBe($key); + expect(File::get($directory.'/.env'))->toBe($contents); + } finally { + app()->useEnvironmentPath($originalPath); + File::deleteDirectory($directory); + } +});