Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/Console/Commands/DevPullCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class DevPullCommand extends Command

protected $description = 'Pull the latest Notur framework code from GitHub for development';

private ?Client $client = null;

public function __construct(?Client $client = null)
{
parent::__construct();
$this->client = $client;
}

public function handle(): int
{
$branch = $this->argument('branch');
Expand All @@ -42,7 +50,7 @@ public function handle(): int
$this->error("The Notur installation directory is not writable: {$noturRoot}. Please adjust filesystem permissions and try again.");
return 1;
}
$client = new Client([
$client = $this->client ?? new Client([
'timeout' => 30,
'connect_timeout' => 10,
'headers' => [
Expand Down
274 changes: 274 additions & 0 deletions tests/Integration/Console/DevPullCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
<?php

declare(strict_types=1);

namespace Notur\Tests\Integration\Console;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Mockery;
use Notur\NoturServiceProvider;
use Orchestra\Testbench\TestCase;

class DevPullCommandTest extends TestCase
{
protected function getPackageProviders($app): array
{
return [NoturServiceProvider::class];
}

protected function getEnvironmentSetUp($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$app['config']->set('notur.repository', 'sak0a/notur');
}

protected function setUp(): void
{
parent::setUp();
$this->loadMigrationsFrom(__DIR__ . '/../../../database/migrations');
}
Comment on lines +33 to +37

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests invoke notur:dev:pull, but the command exits early unless base_path('vendor/notur/notur') exists and is writable. The test setup doesn’t currently create that directory, so the tests will fail before hitting the mocked HTTP client. Create the directory (and ensure it’s writable) in setUp() and clean it up in tearDown() (or use a temporary base path override if your testbench setup supports it).

Copilot uses AI. Check for mistakes.

protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}

public function test_dry_run_shows_what_would_be_done_without_making_changes(): void
{
// Mock the GitHub API response for commit info
$mockClient = Mockery::mock(Client::class);

$mockResponse = new Response(200, [], json_encode([
'sha' => 'abc123def456abc123def456abc123def456abc1',
'commit' => [
'message' => 'Test commit message',
'author' => [
'name' => 'Test Author',
'date' => '2024-01-15T10:30:00Z',
],
],
]));

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/master')
->andReturn($mockResponse);

// Bind the mock client to the service container
$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['--dry-run' => true])
->expectsOutput('[DRY RUN] Would download and extract commit abc123de to ' . base_path('vendor/notur/notur'))
->expectsOutput('[DRY RUN] Would rebuild frontend bridge')
->expectsOutput('[DRY RUN] Would copy bridge.js and tailwind.css to public/notur/')
->assertExitCode(0);
Comment on lines +71 to +75

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectsOutput() is order-sensitive in Laravel/Orchestra console testing; this command prints several lines ("Fetching commit info…", commit details, warnings) before the [DRY RUN] … lines. As written, the first expectsOutput('[DRY RUN] …') is likely to fail because it won’t be the first output line. Prefer expectsOutputToContain() for these assertions, or assert the full ordered output including the preceding lines.

Copilot uses AI. Check for mistakes.
}

public function test_dry_run_with_no_rebuild_option(): void
{
$mockClient = Mockery::mock(Client::class);

$mockResponse = new Response(200, [], json_encode([
'sha' => 'abc123def456abc123def456abc123def456abc1',
'commit' => [
'message' => 'Test commit message',
'author' => [
'name' => 'Test Author',
'date' => '2024-01-15T10:30:00Z',
],
],
]));

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/master')
->andReturn($mockResponse);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['--dry-run' => true, '--no-rebuild' => true])
->expectsOutput('[DRY RUN] Would download and extract commit abc123de to ' . base_path('vendor/notur/notur'))
->doesntExpectOutput('[DRY RUN] Would rebuild frontend bridge')
->assertExitCode(0);
}

public function test_dry_run_with_specific_commit(): void
{
$mockClient = Mockery::mock(Client::class);

$mockResponse = new Response(200, [], json_encode([
'sha' => 'abcd1234567890abcdef1234567890abcdef1234',
'commit' => [
'message' => 'Specific commit message',
'author' => [
'name' => 'Test Author',
'date' => '2024-01-15T10:30:00Z',
],
],
]));

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/specific123')
->andReturn($mockResponse);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['commit' => 'specific123', '--dry-run' => true])
->expectsOutput('[DRY RUN] Would download and extract commit abcd1234 to ' . base_path('vendor/notur/notur'))
->assertExitCode(0);
}

public function test_handles_invalid_ref_error(): void
{
$mockClient = Mockery::mock(Client::class);

$request = new Request('GET', 'https://api.github.com/repos/sak0a/notur/commits/invalid-ref');
$exception = new RequestException(
'Not Found',
$request,
new Response(404, [], '{"message":"Not Found"}')
);

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/invalid-ref')
->andThrow($exception);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['branch' => 'invalid-ref', '--dry-run' => true])
->expectsOutputToContain('Failed to fetch commit info')
->assertExitCode(1);
}

public function test_handles_network_error_on_commit_fetch(): void
{
$mockClient = Mockery::mock(Client::class);

$request = new Request('GET', 'https://api.github.com/repos/sak0a/notur/commits/master');
$exception = new RequestException(
'Connection timeout',
$request
);

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/master')
->andThrow($exception);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['--dry-run' => true])
->expectsOutputToContain('Failed to fetch commit info')
->assertExitCode(1);
}

public function test_handles_malformed_api_response(): void
{
$mockClient = Mockery::mock(Client::class);

// Response missing 'sha' field
$mockResponse = new Response(200, [], json_encode([
'commit' => [
'message' => 'Test commit message',
],
]));

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/master')
->andReturn($mockResponse);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['--dry-run' => true])
->expectsOutputToContain('Failed to fetch commit info')
->assertExitCode(1);
}

public function test_displays_commit_information(): void
{
$mockClient = Mockery::mock(Client::class);

$mockResponse = new Response(200, [], json_encode([
'sha' => 'abc123def456abc123def456abc123def456abc1',
'commit' => [
'message' => "Add new feature\n\nDetailed description here",
'author' => [
'name' => 'Jane Developer',
'date' => '2024-01-15T10:30:00Z',
],
],
]));

$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/sak0a/notur/commits/develop')
->andReturn($mockResponse);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['branch' => 'develop', '--dry-run' => true])
->expectsOutput(' Branch: develop')
->expectsOutput(' Commit: abc123de')
->expectsOutput(' Author: Jane Developer')
->expectsOutput(' Date: 2024-01-15T10:30:00Z')
->expectsOutput(' Message: Add new feature')
Comment on lines +236 to +240

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the dry-run assertions: the command outputs additional lines before the commit info block (e.g. "Fetching commit info…") and also outputs warnings after it. If expectsOutput() is used here, ensure the expected lines match the actual ordered output, or switch these to expectsOutputToContain() to avoid brittle ordering failures.

Suggested change
->expectsOutput(' Branch: develop')
->expectsOutput(' Commit: abc123de')
->expectsOutput(' Author: Jane Developer')
->expectsOutput(' Date: 2024-01-15T10:30:00Z')
->expectsOutput(' Message: Add new feature')
->expectsOutputToContain(' Branch: develop')
->expectsOutputToContain(' Commit: abc123de')
->expectsOutputToContain(' Author: Jane Developer')
->expectsOutputToContain(' Date: 2024-01-15T10:30:00Z')
->expectsOutputToContain(' Message: Add new feature')

Copilot uses AI. Check for mistakes.
->assertExitCode(0);
}

public function test_uses_custom_repository_from_config(): void
{
config(['notur.repository' => 'custom/repo']);

$mockClient = Mockery::mock(Client::class);

$mockResponse = new Response(200, [], json_encode([
'sha' => 'abc123def456abc123def456abc123def456abc1',
'commit' => [
'message' => 'Test commit',
'author' => [
'name' => 'Test Author',
'date' => '2024-01-15T10:30:00Z',
],
],
]));

// Should use custom/repo instead of default
$mockClient->shouldReceive('get')
->once()
->with('https://api.github.com/repos/custom/repo/commits/master')
->andReturn($mockResponse);

$this->app->bind(Client::class, function () use ($mockClient) {
return $mockClient;
});

$this->artisan('notur:dev:pull', ['--dry-run' => true])
->assertExitCode(0);
}
}