@@ -259,7 +259,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
@if (!isCloud() || isDev())
-
@if ($fqdn)
Use {{ $fqdn }}
@@ -279,14 +279,14 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:b
@endif
-
-
-
Register Now
@@ -314,7 +314,7 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:b
-
+
Continue
diff --git a/tests/Feature/CrossTeamDestinationAttachTest.php b/tests/Feature/CrossTeamDestinationAttachTest.php
index e90c8334c..f79c9a1e0 100644
--- a/tests/Feature/CrossTeamDestinationAttachTest.php
+++ b/tests/Feature/CrossTeamDestinationAttachTest.php
@@ -1,5 +1,6 @@
$this->teamA]);
});
+afterEach(function () {
+ GetContainersStatus::clearFake();
+});
+
describe('Destination::addServer GHSA-j395-3pqh-9r5g', function () {
test('cannot attach another team\'s server + network to own application', function () {
try {
@@ -158,4 +164,69 @@
expect($additional->first()->id)->toBe($this->destinationA->id);
expect($additional->first()->pivot->server_id)->toBe($this->serverA->id);
});
+
+ test('refresh failures after promote do not roll back promoted destination', function () {
+ $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
+
+ GetContainersStatus::shouldRun()
+ ->once()
+ ->andThrow(new RuntimeException('refresh failed'));
+
+ try {
+ Livewire::test(Destination::class, ['resource' => $this->applicationA])
+ ->call('promote', $this->destinationA2->id, $this->serverA2->id);
+ } catch (Throwable $e) {
+ // The refresh failure is intentionally outside the transaction; persistence is the assertion.
+ }
+
+ $application = $this->applicationA->fresh();
+ $additional = $application->additional_networks;
+
+ expect($application->destination_id)->toBe($this->destinationA2->id);
+ expect($additional)->toHaveCount(1);
+ expect($additional->first()->id)->toBe($this->destinationA->id);
+ expect($additional->first()->pivot->server_id)->toBe($this->serverA->id);
+ });
+
+ test('only detaches the promoted network for the selected pivot server', function () {
+ $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
+ $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA->id]);
+
+ Livewire::test(Destination::class, ['resource' => $this->applicationA])
+ ->call('promote', $this->destinationA2->id, $this->serverA2->id);
+
+ expect(DB::table('additional_destinations')
+ ->where('application_id', $this->applicationA->id)
+ ->where('standalone_docker_id', $this->destinationA2->id)
+ ->where('server_id', $this->serverA->id)
+ ->exists())->toBeTrue();
+
+ expect(DB::table('additional_destinations')
+ ->where('application_id', $this->applicationA->id)
+ ->where('standalone_docker_id', $this->destinationA2->id)
+ ->where('server_id', $this->serverA2->id)
+ ->exists())->toBeFalse();
+ });
+});
+
+describe('Destination::removeServer', function () {
+ test('only detaches the removed network for the selected pivot server', function () {
+ $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]);
+ $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA->id]);
+
+ Livewire::test(Destination::class, ['resource' => $this->applicationA])
+ ->call('removeServer', $this->destinationA2->id, $this->serverA2->id, 'password');
+
+ expect(DB::table('additional_destinations')
+ ->where('application_id', $this->applicationA->id)
+ ->where('standalone_docker_id', $this->destinationA2->id)
+ ->where('server_id', $this->serverA->id)
+ ->exists())->toBeTrue();
+
+ expect(DB::table('additional_destinations')
+ ->where('application_id', $this->applicationA->id)
+ ->where('standalone_docker_id', $this->destinationA2->id)
+ ->where('server_id', $this->serverA2->id)
+ ->exists())->toBeFalse();
+ });
});
diff --git a/tests/Feature/GithubSourceChangeTest.php b/tests/Feature/GithubSourceChangeTest.php
index 2f79de795..8437f09af 100644
--- a/tests/Feature/GithubSourceChangeTest.php
+++ b/tests/Feature/GithubSourceChangeTest.php
@@ -23,6 +23,23 @@
});
describe('GitHub Source Change Component', function () {
+ test('all github app form controls declare explicit authorization', function () {
+ $view = file_get_contents(resource_path('views/livewire/source/github/change.blade.php'));
+
+ preg_match_all(
+ '/
]*\bcanGate=)[^>]*>/s',
+ $view,
+ $matches,
+ PREG_OFFSET_CAPTURE
+ );
+
+ $missingAuthorization = collect($matches[0])
+ ->map(fn (array $match): string => 'Line '.(substr_count(substr($view, 0, $match[1]), PHP_EOL) + 1).': '.trim(preg_replace('/\s+/', ' ', $match[0])))
+ ->all();
+
+ expect($missingAuthorization)->toBeEmpty();
+ });
+
test('can mount with newly created github app with null app_id', function () {
// Create a GitHub app without app_id (simulating a newly created source)
$githubApp = GithubApp::create([
diff --git a/tests/Feature/SentinelPushDeduplicationTest.php b/tests/Feature/SentinelPushDeduplicationTest.php
index b61e9933c..c14d5c67e 100644
--- a/tests/Feature/SentinelPushDeduplicationTest.php
+++ b/tests/Feature/SentinelPushDeduplicationTest.php
@@ -1,8 +1,10 @@
[['name' => 'app-1', 'state' => 'running', 'health_status' => 'healthy']];
+it('skips dispatch decision when sentinel lock acquisition times out', function () use ($running) {
+ $lock = Mockery::mock();
+ $lock->shouldReceive('block')
+ ->once()
+ ->with(5, Mockery::type('callable'))
+ ->andThrow(LockTimeoutException::class);
+
+ Cache::shouldReceive('lock')
+ ->once()
+ ->with('sentinel:push-lock:'.$this->server->id, 10)
+ ->andReturn($lock);
+
+ $controller = new SentinelController;
+ $method = new ReflectionMethod($controller, 'shouldDispatchUpdate');
+ $method->setAccessible(true);
+
+ expect($method->invoke($controller, $this->server, sentinelPayload($running())))->toBeFalse();
+});
+
it('dispatches the job on the first push', function () use ($running) {
pushSentinel($this->token, sentinelPayload($running()))->assertOk();
diff --git a/tests/Feature/UserRootTeamTest.php b/tests/Feature/UserRootTeamTest.php
new file mode 100644
index 000000000..5adaba6d8
--- /dev/null
+++ b/tests/Feature/UserRootTeamTest.php
@@ -0,0 +1,32 @@
+create(['id' => 0, 'name' => 'Existing Root Team']);
+
+ $rootUser = User::factory()->create(['id' => 0]);
+
+ expect($rootUser->teams()->whereKey(0)->first()?->pivot?->role)->toBe('owner');
+});
+
+it('promotes the root user to owner when the reused root team pivot already exists', function () {
+ Team::factory()->create(['id' => 0, 'name' => 'Existing Root Team']);
+
+ DB::table('team_user')->insert([
+ 'team_id' => 0,
+ 'user_id' => 0,
+ 'role' => 'member',
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ $rootUser = User::factory()->create(['id' => 0]);
+
+ expect($rootUser->teams()->whereKey(0)->first()?->pivot?->role)->toBe('owner');
+});