Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
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
11 changes: 7 additions & 4 deletions lib/Listener/ProductionVersionGuardListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ public function __construct(
*/
public function handle(Event $event): void
{
if (($event instanceof ObjectCreatingEvent) === false
&& ($event instanceof ObjectUpdatingEvent) === false
) {
if ($event instanceof ObjectCreatingEvent) {
$entity = $event->getObject();
} else if ($event instanceof ObjectUpdatingEvent) {
// OR's ObjectUpdatingEvent exposes the new object via getNewObject()
// (not getObject() — the two events have different APIs).
$entity = $event->getNewObject();
} else {
return;
}

$entity = $event->getObject();
$schema = $this->extractSchemaSlug(entity: $entity);
if ($schema !== ApplicationVersionService::APPLICATION_SCHEMA) {
return;
Expand Down
33 changes: 26 additions & 7 deletions tests/Unit/Controller/ApplicationCreationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,6 @@ protected function setUp(): void
userSession: $this->userSession,
);

// Default: authenticated as 'admin'.
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('admin');
$this->userSession->method('getUser')->willReturn($user);

// Default: request returns basic params.
$this->request->method('getParams')->willReturn([
'name' => 'Test App',
Expand All @@ -107,6 +102,22 @@ protected function setUp(): void
]);
}//end setUp()

/**
* Configure the user session to return an authenticated 'admin' user.
*
* PHPUnit 10 does not allow re-configuring a mock method that was already
* stubbed in setUp(). Each test must call this helper explicitly when it
* requires an authenticated session. Tests that expect 401 must NOT call it.
*
* @return void
*/
private function authenticateAsAdmin(): void
{
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('admin');
$this->userSession->method('getUser')->willReturn($user);
}//end authenticateAsAdmin()

// -------------------------------------------------------------------------
// NoAdminRequired attribute
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -134,8 +145,8 @@ public function wizardMethodCarriesNoAdminRequiredAttribute(): void
*/
public function wizardReturns401WhenNoUserSession(): void
{
$this->userSession->method('getUser')->willReturn(null);

// No authenticateAsAdmin() call here — userSession::getUser() returns null
// by default for an unconfigured PHPUnit 10 mock, triggering the 401 branch.
$response = $this->controller->wizard();

self::assertSame(Http::STATUS_UNAUTHORIZED, $response->getStatus());
Expand All @@ -153,6 +164,8 @@ public function wizardReturns401WhenNoUserSession(): void
*/
public function wizardReturns201WithApplicationUuidOnSuccess(): void
{
$this->authenticateAsAdmin();

$this->creationService->method('createApplication')
->willReturn('app-uuid-001');

Expand All @@ -173,6 +186,8 @@ public function wizardReturns201WithApplicationUuidOnSuccess(): void
*/
public function wizardReturns422OnValidationFailure(): void
{
$this->authenticateAsAdmin();

$this->creationService->method('createApplication')
->willThrowException(new WizardCreationException(
errorCode: 'validation_error',
Expand Down Expand Up @@ -201,6 +216,8 @@ public function wizardReturns422OnValidationFailure(): void
*/
public function wizardReturns500OnRollbackComplete(): void
{
$this->authenticateAsAdmin();

$this->creationService->method('createApplication')
->willThrowException(new WizardCreationException(
errorCode: 'wizard_rollback',
Expand Down Expand Up @@ -229,6 +246,8 @@ public function wizardReturns500OnRollbackComplete(): void
*/
public function wizardReturns500WithOrphanedResourcesOnRollbackPartial(): void
{
$this->authenticateAsAdmin();

$this->creationService->method('createApplication')
->willThrowException(new WizardCreationException(
errorCode: 'wizard_rollback',
Expand Down
18 changes: 14 additions & 4 deletions tests/Unit/Controller/CreateFromTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
namespace OCA\OpenBuilt\Tests\Unit\Controller;

use OCA\OpenBuilt\Controller\ApplicationsController;
use OCA\OpenBuilt\Service\ManifestResolverService;
use OCA\OpenRegister\Db\ObjectEntity;
use OCA\OpenRegister\Db\Register;
use OCA\OpenRegister\Db\RegisterMapper;
Expand Down Expand Up @@ -108,6 +109,13 @@ class CreateFromTemplateTest extends TestCase
*/
private IGroupManager&MockObject $groupManager;

/**
* Mock ManifestResolverService (unused by createFromTemplate but required by the ctor).
*
* @var ManifestResolverService&MockObject
*/
private ManifestResolverService&MockObject $manifestResolver;

/**
* Per-app Register entity stub.
*
Expand All @@ -131,10 +139,11 @@ protected function setUp(): void
{
parent::setUp();

$this->request = $this->createMock(IRequest::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->request = $this->createMock(IRequest::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->manifestResolver = $this->createMock(ManifestResolverService::class);

$this->objectService = $this->createMock(ObjectService::class);

Expand Down Expand Up @@ -203,6 +212,7 @@ function (...$args) use ($applicationTemplateSchema, $applicationSchema): Schema
schemaMapper: $this->schemaMapper,
userSession: $this->userSession,
groupManager: $this->groupManager,
manifestResolver: $this->manifestResolver,
auditTrailMapper: null,
);
}//end setUp()
Expand Down
35 changes: 26 additions & 9 deletions tests/Unit/Service/ApplicationCreationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,30 +369,47 @@ public function createApplicationReturnUuidOnSinglePresetSuccess(): void
*/
private function stubSuccessfulCreation(string $appUuid, array $versionUuids): void
{
// Build the mock register.
$mockRegister = $this->createMock(Register::class);
// Build the mock register. Register::getId() resolves via Entity::__call so it must
// be declared through addMethods() rather than mocked via createMock() alone.
$mockRegister = $this->getMockBuilder(Register::class)
->disableOriginalConstructor()
->onlyMethods(['getSchemas', 'setSchemas'])
->addMethods(['getId'])
->getMock();
$mockRegister->method('getSchemas')->willReturn([]);
$mockRegister->method('getId')->willReturn(1);

$this->registerMapper->method('find')->willReturn($mockRegister);
$this->registerMapper->method('createFromArray')->willReturn($mockRegister);
$this->registerMapper->method('update')->willReturn($mockRegister);

// Build mock schema.
$mockSchema = $this->createMock(Schema::class);
// Build mock schema. Schema::getId() resolves via Entity::__call so it must be
// declared through addMethods() rather than mocked via createMock() alone.
$mockSchema = $this->getMockBuilder(Schema::class)
->disableOriginalConstructor()
->addMethods(['getId'])
->getMock();
$mockSchema->method('getId')->willReturn(1);
$this->schemaMapper->method('find')->willReturn($mockSchema);
$this->schemaMapper->method('createFromArray')->willReturn($mockSchema);

// Prepare the sequential return values.
$appResult = ['id' => $appUuid, 'uuid' => $appUuid];
$versionResults = [];
// Build ObjectEntity mocks for saveObject return values.
// saveObject() must return an ObjectEntity; normaliseObject() calls jsonSerialize()
// on the result to extract the UUID, so each mock's jsonSerialize() returns the payload.
$makeEntity = function (array $payload): \OCA\OpenRegister\Db\ObjectEntity {
$entity = $this->createMock(\OCA\OpenRegister\Db\ObjectEntity::class);
$entity->method('jsonSerialize')->willReturn($payload);
return $entity;
};

$appEntity = $makeEntity(['id' => $appUuid, 'uuid' => $appUuid]);
$versionEntities = [];
foreach ($versionUuids as $slug => $uuid) {
$versionResults[] = ['id' => $uuid, 'uuid' => $uuid, 'slug' => $slug];
$versionEntities[] = $makeEntity(['id' => $uuid, 'uuid' => $uuid, 'slug' => $slug]);
}

// Map saveObject call sequence: app, then versions (×2 for create+wiring), then productionVersion update.
$callQueue = [$appResult, ...$versionResults, ...array_fill(0, count($versionUuids), $appResult), $appResult];
$callQueue = [$appEntity, ...$versionEntities, ...array_fill(0, count($versionUuids), $appEntity), $appEntity];

$callIndex = 0;
$this->objectService->method('saveObject')
Expand Down
24 changes: 15 additions & 9 deletions tests/Unit/Service/ManifestResolverServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,16 @@ public function testResolveReturnsManifestForOwner(): void
{
$manifest = ['version' => '1.0.0', 'pages' => ['home']];
$application = [
'uuid' => 'app-uuid',
'slug' => 'hello-world',
'productionVersion' => 'prod-uuid',
'permissions' => ['owners' => ['user:alice'], 'editors' => []],
];
$version = [
'uuid' => 'staging-uuid',
'slug' => 'staging',
'manifest' => $manifest,
'uuid' => 'staging-uuid',
'slug' => 'staging',
'application' => 'app-uuid',
'manifest' => $manifest,
];

$this->objectService->method('searchObjects')
Expand Down Expand Up @@ -324,14 +326,16 @@ public function testResolveReturnsManifestForEditor(): void
{
$manifest = ['version' => '1.0.0', 'pages' => []];
$application = [
'uuid' => 'app-uuid',
'slug' => 'hello-world',
'productionVersion' => 'prod-uuid',
'permissions' => ['owners' => [], 'editors' => ['user:bob']],
];
$version = [
'uuid' => 'staging-uuid',
'slug' => 'staging',
'manifest' => $manifest,
'uuid' => 'staging-uuid',
'slug' => 'staging',
'application' => 'app-uuid',
'manifest' => $manifest,
];

$this->objectService->method('searchObjects')
Expand Down Expand Up @@ -364,15 +368,17 @@ public function testResolveAllowsAnyCallerOnProductionVersion(): void
{
$manifest = ['version' => '2.0.0', 'pages' => ['home', 'about']];
$application = [
'uuid' => 'app-uuid',
'slug' => 'hello-world',
'productionVersion' => 'prod-uuid',
'permissions' => ['owners' => [], 'editors' => []],
];
// The requested version IS the production version.
$prodVersion = [
'uuid' => 'prod-uuid',
'slug' => 'production',
'manifest' => $manifest,
'uuid' => 'prod-uuid',
'slug' => 'production',
'application' => 'app-uuid',
'manifest' => $manifest,
];

$this->objectService->method('searchObjects')
Expand Down
20 changes: 15 additions & 5 deletions tests/stubs/openregister-stubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,10 @@ public function getErrors(): array

if (class_exists(ObjectUpdatingEvent::class, autoload: false) === false) {
/**
* Stub ObjectUpdatingEvent — same shape as ObjectCreatingEvent.
* Stub ObjectUpdatingEvent — exposes the new object via getNewObject()
* (the real OR class signature is `__construct(ObjectEntity $newObject,
* ?ObjectEntity $oldObject = null)`). Tests that previously called
* `$event->getObject()` SHALL be updated to `getNewObject()`.
*/
class ObjectUpdatingEvent extends \OCP\EventDispatcher\Event implements \Psr\EventDispatcher\StoppableEventInterface
{
Expand All @@ -590,14 +593,21 @@ class ObjectUpdatingEvent extends \OCP\EventDispatcher\Event implements \Psr\Eve
*/
private array $errors = [];

public function __construct(private readonly \OCA\OpenRegister\Db\ObjectEntity $object)
{
public function __construct(
private readonly \OCA\OpenRegister\Db\ObjectEntity $newObject,
private readonly ?\OCA\OpenRegister\Db\ObjectEntity $oldObject = null,
) {
parent::__construct();
}

public function getObject(): \OCA\OpenRegister\Db\ObjectEntity
public function getNewObject(): \OCA\OpenRegister\Db\ObjectEntity
{
return $this->object;
return $this->newObject;
}

public function getOldObject(): ?\OCA\OpenRegister\Db\ObjectEntity
{
return $this->oldObject;
}

public function isPropagationStopped(): bool
Expand Down