1
<?php
2
declare(strict_types=1);
3
4
use Machweis\Backend\Auth\TokenService;
5
use Machweis\Backend\Config;
6
use Machweis\Backend\EmployeeProfileService;
7
use Machweis\Backend\Kernel;
8
use Machweis\Backend\Logger;
9
use Machweis\Backend\Request;
10
11
require dirname(__DIR__) . '/src/autoload.php';
12
13
function invitationCheck(bool $condition, string $message): void { if (!$condition) throw new RuntimeException($message); }
14
function invitationUuid(): string { $hex = bin2hex(random_bytes(16)); return substr($hex,0,8).'-'.substr($hex,8,4).'-4'.substr($hex,13,3).'-8'.substr($hex,17,3).'-'.substr($hex,20); }
15
16
$config = new Config('test', false, (string)getenv('DB_HOST'), (int)getenv('DB_PORT'), (string)getenv('DB_NAME'), (string)getenv('DB_USER'), (string)getenv('DB_PASSWORD'), [], true, str_repeat('invitation-contract-secret-', 2));
17
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $config->dbHost, $config->dbPort, $config->dbName), $config->dbUser, $config->dbPassword, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false]);
18
$tenantA = invitationUuid(); $tenantB = invitationUuid(); $owner = invitationUuid(); $office = invitationUuid();
19
$pdo->prepare('INSERT INTO tenants (id,name,status,created_at) VALUES (?, ?, ?, UTC_TIMESTAMP()), (?, ?, ?, UTC_TIMESTAMP())')->execute([$tenantA,'Invitation A','active',$tenantB,'Invitation B','active']);
20
$pdo->prepare('INSERT INTO users (id,email,password_hash,status,created_at) VALUES (?, ?, ?, ?, UTC_TIMESTAMP()), (?, ?, ?, ?, UTC_TIMESTAMP())')->execute([$owner,'owner-'.substr($owner,0,8).'@example.test',password_hash('owner-password', PASSWORD_ARGON2ID),'active',$office,'office-'.substr($office,0,8).'@example.test',password_hash('office-password', PASSWORD_ARGON2ID),'active']);
21
$ownerRole = $pdo->query("SELECT id FROM roles WHERE code='owner'")->fetchColumn(); $officeRole = $pdo->query("SELECT id FROM roles WHERE code='office'")->fetchColumn();
22
$pdo->prepare('INSERT INTO tenant_memberships (id,tenant_id,user_id,role_id,status,created_at) VALUES (?, ?, ?, ?, ?, UTC_TIMESTAMP()), (?, ?, ?, ?, ?, UTC_TIMESTAMP())')->execute([invitationUuid(),$tenantA,$owner,$ownerRole,'active',invitationUuid(),$tenantA,$office,$officeRole,'active']);
23
$tokens = new TokenService($config); $kernel = new Kernel($config, new Logger('test')); $ownerToken = $tokens->issueAccess(['sub'=>$owner,'tid'=>$tenantA,'role'=>'owner']); $officeToken = $tokens->issueAccess(['sub'=>$office,'tid'=>$tenantA,'role'=>'office']);
24
$create = $kernel->handle(new Request('POST','/api/v1/invitations',['authorization'=>'Bearer '.$ownerToken],json_encode(['email'=>'new.employee@example.test','firstName'=>'Nina','lastName'=>'Neu','employeeNumber'=>'M-100','phone'=>'+49 30 123','role'=>'supervisor'], JSON_THROW_ON_ERROR)));
25
invitationCheck($create->status === 201, 'owner must create an invitation'); $invitation = $create->body['data']; $token = (string)$invitation['token']; invitationCheck($token !== '' && strlen($token) >= 40, 'invitation token must be returned once');
26
$inspect = $kernel->handle(new Request('POST','/api/v1/invitations/inspect',[],json_encode(['token'=>$token], JSON_THROW_ON_ERROR))); invitationCheck($inspect->status === 200 && $inspect->body['data']['email'] === 'new.employee@example.test' && $inspect->body['data']['role'] === 'supervisor', 'invitation details must be inspectable without authentication');
27
$list = $kernel->handle(new Request('GET','/api/v1/invitations',['authorization'=>'Bearer '.$officeToken])); invitationCheck($list->status === 200 && count($list->body['data']['items']) === 1 && !isset($list->body['data']['items'][0]['token']), 'admins may list invitations without exposing tokens');
28
$accept = $kernel->handle(new Request('POST','/api/v1/invitations/accept',[],json_encode(['token'=>$token,'password'=>'a-very-secure-password'], JSON_THROW_ON_ERROR))); invitationCheck($accept->status === 200 && $accept->body['data']['role'] === 'supervisor', 'acceptance must create and sign in the invited account');
29
$acceptedUser = (string) ($accept->body['data']['user']['id'] ?? ''); invitationCheck($acceptedUser !== '', 'acceptance must return the created user');
30
$profile = $pdo->prepare('SELECT user_id FROM employee_profiles WHERE tenant_id=? AND email=?'); $profile->execute([$tenantA,'new.employee@example.test']); invitationCheck(is_string($profile->fetchColumn()), 'accepted invitation must link the user to the employee profile');
31
$employees = (new EmployeeProfileService($pdo))->list($tenantA, '', 1, 10); invitationCheck($employees['items'][0]['role'] === 'supervisor', 'employee role must come from the linked membership');
32
$replay = $kernel->handle(new Request('POST','/api/v1/invitations/accept',[],json_encode(['token'=>$token,'password'=>'a-very-secure-password'], JSON_THROW_ON_ERROR))); invitationCheck($replay->status === 422, 'accepted invitations must be single use');
33
$forbidden = $kernel->handle(new Request('POST','/api/v1/invitations',['authorization'=>'Bearer '.$tokens->issueAccess(['sub'=>invitationUuid(),'tid'=>$tenantA,'role'=>'supervisor'])],json_encode(['email'=>'blocked@example.test','firstName'=>'Blocked','lastName'=>'User','employeeNumber'=>'M-101'], JSON_THROW_ON_ERROR))); invitationCheck($forbidden->status === 403, 'non-admin roles must not create invitations');
34
$invalid = $kernel->handle(new Request('POST','/api/v1/invitations/inspect',[],json_encode(['token'=>'invalid'], JSON_THROW_ON_ERROR))); invitationCheck($invalid->status === 422, 'invalid invitation must be rejected');
35
$pdo->prepare('DELETE FROM refresh_tokens WHERE user_id IN (?,?,?)')->execute([$owner,$office,$acceptedUser]); $pdo->prepare('DELETE FROM employee_invitations WHERE tenant_id IN (?,?)')->execute([$tenantA,$tenantB]); $pdo->prepare('DELETE FROM employee_profiles WHERE tenant_id IN (?,?)')->execute([$tenantA,$tenantB]); $pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id IN (?,?)')->execute([$tenantA,$tenantB]); $pdo->prepare('DELETE FROM users WHERE id IN (?,?) OR email=?')->execute([$owner,$office,'new.employee@example.test']); $pdo->prepare('DELETE FROM tenants WHERE id IN (?,?)')->execute([$tenantA,$tenantB]);
36
fwrite(STDOUT, "invitations_contract: OK\n");
37