Skip to content

Commit 8fd9130

Browse files
authored
Merge pull request #535 from ConductionNL/fix/green-softwarecatalog
fix: close the ADR-084 contract drift behind 19 PHPUnit failures and 44 static findings
2 parents 5380993 + e58efc9 commit 8fd9130

25 files changed

Lines changed: 324 additions & 305 deletions

lib/AppInfo/Application.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,6 @@ function ($container) {
377377
return new GebruikSyncService(
378378
logger: $container->get('Psr\Log\LoggerInterface'),
379379
settingsService: $container->get(SettingsService::class),
380-
container: $container,
381380
objectService: $container->get(ObjectServiceInterface::class),
382381
);
383382
}
@@ -738,7 +737,6 @@ function ($container) {
738737
userManager: $container->get('OCP\IUserManager'),
739738
groupManager: $container->get('OCP\IGroupManager'),
740739
userSession: $container->get('OCP\IUserSession'),
741-
container: $container,
742740
secureRandom: $container->get('OCP\Security\ISecureRandom'),
743741
logger: $container->get('Psr\Log\LoggerInterface'),
744742
// ADR-084 added these two to the constructor; this hand-written

lib/Controller/ContactpersonenController.php

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
use OCP\IUserManager;
3434
use OCP\IUserSession;
3535
use OCP\Security\ISecureRandom;
36-
use Psr\Container\ContainerInterface;
3736
use Psr\Log\LoggerInterface;
3837
use OCA\OpenRegister\Contract\ObjectServiceInterface;
3938
use OCA\OpenRegister\Service\OrganisationService;
@@ -111,13 +110,6 @@ class ContactpersonenController extends Controller {
111110
*/
112111
private IUserSession $userSession;
113112

114-
/**
115-
* Container for dependency injection.
116-
*
117-
* @var ContainerInterface
118-
*/
119-
private ContainerInterface $container;
120-
121113
/**
122114
* Contactpersoon service for business logic.
123115
*
@@ -136,7 +128,6 @@ class ContactpersonenController extends Controller {
136128
* @param IUserManager $userManager User manager
137129
* @param IGroupManager $groupManager Group manager
138130
* @param IUserSession $userSession User session
139-
* @param ContainerInterface $container Container for DI
140131
* @param ISecureRandom $secureRandom Secure random generator
141132
* @param LoggerInterface $logger Logger instance
142133
* @param ObjectServiceInterface $objectService OpenRegister object access (ADR-022/ADR-084 —
@@ -154,7 +145,6 @@ public function __construct(
154145
IUserManager $userManager,
155146
IGroupManager $groupManager,
156147
IUserSession $userSession,
157-
ContainerInterface $container,
158148
ISecureRandom $secureRandom,
159149
LoggerInterface $logger,
160150
private readonly ObjectServiceInterface $objectService,
@@ -167,7 +157,6 @@ public function __construct(
167157
$this->userManager = $userManager;
168158
$this->groupManager = $groupManager;
169159
$this->userSession = $userSession;
170-
$this->container = $container;
171160
$this->secureRandom = $secureRandom;
172161
$this->logger = $logger;
173162
}//end __construct()
@@ -515,7 +504,12 @@ public function convertToUser(string $contactPersonId): JSONResponse {
515504

516505
$contactData = $this->normaliseContactDataForPersist(contactData: $contactData);
517506

518-
$contactPersonObject->setObject($contactData);
507+
// No local `setObject()` on the entity here. `setObject()` is not on
508+
// the published ObjectEntityInterface (ADR-084) — it is an
509+
// implementation-only accessor reached through Entity::__call() — and
510+
// mutating the in-memory copy changed nothing anyway: the save below
511+
// sends `$contactData` itself, and the entity is only read again for
512+
// its UUID.
519513

520514
// Debug logging to understand data types before save.
521515
$lastNameValue = $contactData['achternaam'] ?? 'not set';
@@ -550,8 +544,7 @@ public function convertToUser(string $contactPersonId): JSONResponse {
550544
schema: $schemaId,
551545
uuid: $contactPersonObject->getUuid(),
552546
silent: true,
553-
silent: true,
554-
_validation: false
547+
_validation: false
555548
);
556549

557550
$this->logger->info(

lib/EventListener/UserProfileUpdatedEventListener.php

Lines changed: 24 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@
2626
use Psr\Container\ContainerInterface;
2727
use Psr\Log\LoggerInterface;
2828
use OCA\OpenRegister\Contract\ObjectServiceInterface;
29-
use OCA\OpenRegister\Db\SchemaMapper;
30-
use OCA\OpenRegister\Db\RegisterMapper;
31-
use OCA\OpenRegister\Service\Object\SaveObject\MetadataHydrationHandler;
3229

3330
/**
3431
* Syncs user profile changes to the corresponding contactpersoon object.
@@ -58,16 +55,10 @@ class UserProfileUpdatedEventListener implements IEventListener {
5855
*
5956
* @param ContainerInterface $container DI container for lazy service resolution.
6057
* @param ObjectServiceInterface $objectService OpenRegister object access (ADR-084 contract).
61-
* @param SchemaMapper $schemaMapper Resolves the contactpersoon schema.
62-
* @param RegisterMapper $registerMapper Resolves the register the schema lives in.
63-
* @param MetadataHydrationHandler $metadataHydrationHandler Hydrates `@self` metadata on read.
6458
*/
6559
public function __construct(
6660
private readonly ContainerInterface $container,
6761
private readonly ObjectServiceInterface $objectService,
68-
private readonly SchemaMapper $schemaMapper,
69-
private readonly RegisterMapper $registerMapper,
70-
private readonly MetadataHydrationHandler $metadataHydrationHandler,
7162
) {
7263
}//end __construct()
7364

@@ -207,16 +198,16 @@ private function syncToContactPerson(UserProfileUpdatedEvent $event, LoggerInter
207198
]
208199
);
209200

210-
// Merge the patch into existing data and save directly via mapper to skip schema validation.
211-
// Schema validation can reject existing data with legacy values (e.g. notificaties enum).
201+
// Merge the patch into the existing payload. `saveObject()` is
202+
// PUT-semantic, so the FULL payload must be carried forward, not only
203+
// the changed keys. Validation is skipped on the save because schema
204+
// validation can reject pre-existing legacy values (e.g. notificaties
205+
// enum) that this listener never touched.
212206
$mergedObject = array_merge($contactData, $patch);
213-
$contactPerson->setObject($mergedObject);
214207

215208
$this->persistContactPersonPatch(
216209
contactPerson: $contactPerson,
217-
register: (int)$register,
218-
schema: (int)$contactPersonSchema,
219-
logger: $logger
210+
object: $mergedObject
220211
);
221212

222213
$logger->info(
@@ -278,63 +269,39 @@ private function buildContactPatch(
278269
}//end buildContactPatch()
279270

280271
/**
281-
* Persist the patched contactpersoon entity, regenerating `_name`
282-
* metadata first when the schema is loadable.
272+
* Persist the patched contactpersoon payload through the published
273+
* OpenRegister contract.
283274
*
284-
* @param object $contactPerson The contactpersoon entity.
285-
* @param int $register The voorzieningen register id.
286-
* @param int $schema The contactpersoon schema id.
287-
* @param LoggerInterface $logger Logger for hydration warnings.
275+
* `_name` metadata is NOT regenerated here. This method used to load the
276+
* schema through `SchemaMapper` and call
277+
* `MetadataHydrationHandler::hydrateObjectMetadata()` itself — both of them
278+
* OpenRegister internals that ADR-084's published contract deliberately does
279+
* not expose, and neither of which a leaf app can load in its own unit
280+
* tests. It was also redundant: `ObjectService::saveObject()` calls
281+
* `hydrateObjectMetadata()` on both its create and its update path before
282+
* handing the entity to `objectEntityMapper`, so the hydration happens
283+
* exactly once either way — it was simply being done twice, one layer too
284+
* deep.
285+
*
286+
* @param object $contactPerson The contactpersoon entity (read for its coordinates).
287+
* @param array $object The full merged payload to store (PUT-semantic).
288288
*
289289
* @return void
290290
*/
291291
private function persistContactPersonPatch(
292292
object $contactPerson,
293-
int $register,
294-
int $schema,
295-
LoggerInterface $logger,
293+
array $object,
296294
): void {
297-
$schemaEntity = null;
298-
$registerEntity = null;
299-
try {
300-
$schemaEntity = $this->schemaMapper->find(id: $schema, _rbac: false, _multitenancy: false);
301-
$registerEntity = $this->registerMapper->find(id: $register, _rbac: false, _multitenancy: false);
302-
} catch (\Exception $e) {
303-
$logger->warning(
304-
'[UserProfileUpdatedEventListener] Could not load schema/register entities for _name hydration',
305-
[
306-
'error' => $e->getMessage(),
307-
]
308-
);
309-
}
310-
311-
if ($schemaEntity !== null) {
312-
$this->metadataHydrationHandler->hydrateObjectMetadata(entity: $contactPerson, schema: $schemaEntity);
313-
$logger->debug(
314-
'[UserProfileUpdatedEventListener] Regenerated _name metadata',
315-
[
316-
'newName' => $contactPerson->getName(),
317-
]
318-
);
319-
}
320-
321-
// Persist through the published contract rather than OpenRegister's Db
322-
// layer. The comment this replaces worried that a plain save would touch
323-
// "just the blob table" — it does not: ObjectService::saveObject() calls
324-
// metaHydrationHandler->hydrateObjectMetadata() and then
325-
// objectEntityMapper->update(entity:, register:, schema:), which IS the
326-
// magic-mapper route. This listener was hand-rolling OpenRegister's own
327-
// save pipeline, one layer too deep.
328295
$this->objectService->saveObject(
329-
object: $contactPerson->getObject(),
296+
object: $object,
330297
register: $contactPerson->getRegister(),
331298
schema: $contactPerson->getSchema(),
332299
uuid: $contactPerson->getUuid(),
333300
silent: true,
334301
_validation: false
335302
);
336303

337-
}//end persistContactpersoonPatch()
304+
}//end persistContactPersonPatch()
338305

339306
/**
340307
* Find a contactpersoon by username, falling back to a case-insensitive email search.

lib/Service/AanbodService.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,12 @@ public function acceptAanbod(string $aanbodId, array $options = []): array {
374374
);
375375

376376
// Save the updated object with RBAC and multitenancy disabled.
377-
$existingAanbod->setObject($aanbodData);
377+
// The PAYLOAD is what is sent — `setObject()` on the entity is not on
378+
// the published ObjectEntityInterface (ADR-084) and the mutated copy
379+
// was never read back anyway; `saveObject()` is PUT-semantic, so
380+
// $aanbodData already carries every unchanged field forward.
378381
$updatedAanbod = $objectService->saveObject(
379-
object: $existingAanbod,
382+
object: $aanbodData,
380383
register: $existingAanbod->getRegister(),
381384
schema: $existingAanbod->getSchema(),
382385
uuid: $aanbodId,
@@ -593,12 +596,12 @@ private function resolvePartyId(mixed $partyInfo): ?string {
593596
* @param ObjectServiceInterface $objectService The OpenRegister object service
594597
* @param string $aanbodId The UUID of the aanbod object
595598
*
596-
* @return \OCA\OpenRegister\Db\ObjectEntity|null The found object or null
599+
* @return \OCA\OpenRegister\Contract\ObjectEntityInterface|null The found object or null
597600
*/
598601
private function findAanbodObject(
599602
ObjectServiceInterface $objectService,
600603
string $aanbodId,
601-
): ?\OCA\OpenRegister\Db\ObjectEntity {
604+
): ?\OCA\OpenRegister\Contract\ObjectEntityInterface {
602605
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
603606
$registerId = $voorzieningenConfig['register'] ?? null;
604607

lib/Service/AangebodenGebruikService.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -992,9 +992,11 @@ public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []
992992

993993
// Save the updated object with RBAC and multitenancy disabled.
994994
// Use register/schema from the found entity for correct table routing.
995-
$existingGebruik->setObject($gebruikData);
995+
// The PAYLOAD is what is sent — `setObject()` is not on the published
996+
// ObjectEntityInterface (ADR-084), and `saveObject()` is PUT-semantic,
997+
// so $gebruikData already carries every unchanged field forward.
996998
$updatedGebruik = $objectService->saveObject(
997-
object: $existingGebruik,
999+
object: $gebruikData,
9981000
register: $existingGebruik->getRegister(),
9991001
schema: $existingGebruik->getSchema(),
10001002
uuid: $gebruikId,
@@ -1049,12 +1051,12 @@ public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []
10491051
* @param ObjectServiceInterface $objectService The OpenRegister object service
10501052
* @param string $objectId The UUID of the object to find
10511053
*
1052-
* @return \OCA\OpenRegister\Db\ObjectEntity|null The found object or null
1054+
* @return \OCA\OpenRegister\Contract\ObjectEntityInterface|null The found object or null
10531055
*/
10541056
private function findGebruikOrIntegration(
10551057
ObjectServiceInterface $objectService,
10561058
string $objectId,
1057-
): ?\OCA\OpenRegister\Db\ObjectEntity {
1059+
): ?\OCA\OpenRegister\Contract\ObjectEntityInterface {
10581060
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
10591061
$registerId = $voorzieningenConfig['register'] ?? null;
10601062

lib/Service/ContactpersoonService.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler;
2626
use OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler;
2727
use OCP\App\IAppManager;
28+
use OCP\AppFramework\Db\DoesNotExistException;
2829
use OCP\IAppConfig;
2930
use Psr\Container\ContainerInterface;
3031
use Psr\Log\LoggerInterface;
@@ -1026,15 +1027,24 @@ public function getBulkUserInfo(array $contactPersonIds): array {
10261027
}
10271028

10281029
// Find the contactpersoon object with register and schema specified.
1029-
$contactObject = $objectService->findSilent(
1030-
id: $contactPersonId,
1031-
_extend: [],
1032-
files: false,
1033-
register: $contactRegister,
1034-
schema: $contactSchema
1035-
);
1036-
1037-
if ($contactObject === null) {
1030+
//
1031+
// A MISS RAISES, it does not return null: `findSilent()` is
1032+
// declared `ObjectEntityInterface` (non-nullable) on the
1033+
// published contract and OpenRegister lets the mapper's
1034+
// DoesNotExistException out. The `=== null` test this replaces
1035+
// could therefore never be true — the distinct "not found"
1036+
// entry below was unreachable, and every missing contactpersoon
1037+
// fell through to the generic error arm and came back carrying
1038+
// an `error` key instead.
1039+
try {
1040+
$contactObject = $objectService->findSilent(
1041+
id: $contactPersonId,
1042+
_extend: [],
1043+
files: false,
1044+
register: $contactRegister,
1045+
schema: $contactSchema
1046+
);
1047+
} catch (DoesNotExistException $e) {
10381048
$this->logger->warning(
10391049
'ContactpersoonService: Contactpersoon not found for bulk user info',
10401050
['contactpersoonId' => $contactPersonId]
@@ -1045,7 +1055,7 @@ public function getBulkUserInfo(array $contactPersonIds): array {
10451055
'groups' => [],
10461056
];
10471057
continue;
1048-
}
1058+
}//end try
10491059

10501060
$contactData = $contactObject->getObject();
10511061
$username = $contactData['username'] ?? null;

lib/Service/ContractApprovalService.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
namespace OCA\SoftwareCatalog\Service;
3939

4040
use OCA\OpenRegister\Contract\ObjectServiceInterface;
41-
use OCA\OpenRegister\Service\ObjectService;
4241
use OCP\EventDispatcher\IEventDispatcher;
4342
use Psr\Container\ContainerInterface;
4443
use Psr\Log\LoggerInterface;
@@ -501,7 +500,7 @@ private function buildSubjectLabel(array $data): string {
501500
*
502501
* @param string $contractUuid The contract uuid.
503502
*
504-
* @return \OCA\OpenRegister\Db\ObjectEntity|null The object, or null.
503+
* @return \OCA\OpenRegister\Contract\ObjectEntityInterface|null The object, or null.
505504
*
506505
* @spec openspec/specs/contract-decision-delegation/spec.md
507506
*/
@@ -540,7 +539,7 @@ private function loadContract(string $contractUuid) {
540539
/**
541540
* Persist a mutated contract object back to the OR store.
542541
*
543-
* @param \OCA\OpenRegister\Db\ObjectEntity $contract The contract entity.
542+
* @param \OCA\OpenRegister\Contract\ObjectEntityInterface $contract The contract entity.
544543
* @param array $data The mutated object data.
545544
*
546545
* @return void
@@ -572,8 +571,14 @@ private function persistContract($contract, array $data): void {
572571
*/
573572
private function getObjectService(): ?ObjectServiceInterface {
574573
try {
575-
$service = $this->container->get('OCA\OpenRegister\Service\ObjectService');
576-
if ($service instanceof ObjectService) {
574+
// Ask for the CONTRACT and narrow on the CONTRACT (ADR-084). Asking
575+
// for the concrete class and gating on `instanceof ObjectService`
576+
// made this method return null for anything that satisfies the
577+
// published interface without being that exact class — which is
578+
// every double a leaf app can build, so the ownership check silently
579+
// fell through to its fail-closed arm and refused an owner.
580+
$service = $this->container->get(ObjectServiceInterface::class);
581+
if ($service instanceof ObjectServiceInterface) {
577582
return $service;
578583
}
579584
} catch (\Throwable $e) {

lib/Service/ContractStatusService.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
use DateTimeImmutable;
3030
use OCA\OpenRegister\Contract\ObjectServiceInterface;
31-
use OCA\OpenRegister\Service\ObjectService;
3231
use Psr\Container\ContainerInterface;
3332
use Psr\Log\LoggerInterface;
3433

@@ -189,8 +188,11 @@ public function expirePastContracts(?DateTimeImmutable $now = null): int {
189188
*/
190189
private function getObjectService(): ?ObjectServiceInterface {
191190
try {
192-
$service = $this->container->get('OCA\OpenRegister\Service\ObjectService');
193-
if ($service instanceof ObjectService) {
191+
// Ask for the CONTRACT and narrow on the CONTRACT (ADR-084) — see
192+
// ContractApprovalService::getObjectService() for why gating on the
193+
// concrete class is a silent fail-closed.
194+
$service = $this->container->get(ObjectServiceInterface::class);
195+
if ($service instanceof ObjectServiceInterface) {
194196
return $service;
195197
}
196198
} catch (\Throwable $e) {

0 commit comments

Comments
 (0)