setModuleConfig('reCAPTCHA', 'enabled', 'false'); $user = api_fixtures()->createUser([ 'display_name' => 'API Login User', 'password_plaintext' => 'Secret123!', ]); $response = api_client()->post('/auth/login', [ 'customer_number' => $user['customer_number'], 'password' => $user['password_plaintext'], ]); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()) ->toBeArray() ->toHaveKey('token') ->and($response->data()['token']) ->toBeString() ->toHaveLength(64); }); it('rejects invalid login payloads and credentials', function (): void { api_test_covers('POST /auth/login', 'failure'); api_fixtures()->setModuleConfig('reCAPTCHA', 'enabled', 'false'); $user = api_fixtures()->createUser([ 'password_plaintext' => 'Secret123!', ]); $missingPassword = api_client()->post('/auth/login', [ 'customer_number' => $user['customer_number'], ]); $missingPassword ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Missing required parameters: password'); $invalidCredentials = api_client()->post('/auth/login', [ 'customer_number' => $user['customer_number'], 'password' => 'wrong-password', ]); $invalidCredentials ->assertStatus(401) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid credentials'); }); it('returns the cached auth session payload for a valid token', function (): void { api_test_covers('GET /auth/session', 'happy'); $session = api_fixtures()->createUserSession(['list_departments'], [ 'display_name' => 'Session User', ]); api_fixtures()->cacheAuthSessionForUser( $session['user'], $session['token'], ['list_departments'], [ 'two_factor_enabled' => false, ] ); api_fixtures()->setModuleConfig('economic', 'transactionDraftCustomerNumber', '445566', 'int'); api_fixtures()->setModuleConfig('economic', 'defaultDepartmentId', '75', 'int'); $response = api_client()->get('/auth/session', $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()) ->toBeArray() ->toHaveKey('customer_number', $session['user']['customer_number']) ->toHaveKey('two_factor_enabled', false) ->and($response->data()['permissions']) ->toContain('list_departments') ->and($response->data()['runtime_config']['economic']['transaction_draft_customer_number'] ?? null) ->toBe(445566) ->and($response->data()['runtime_config']['economic']['default_distribution_department_id'] ?? null) ->toBe(75); }); it('includes economic runtime config for uncached auth sessions', function (): void { api_test_covers('GET /auth/session', 'happy'); api_fixtures()->setModuleConfig('economic', 'transactionDraftCustomerNumber', '556677', 'int'); api_fixtures()->setModuleConfig('economic', 'defaultDepartmentId', '65', 'int'); $session = api_fixtures()->createUserSession(['list_departments'], [ 'display_name' => 'Fresh Session User', ]); api_fixtures()->addCustomerAttribute((int)$session['user']['id'], 'restrictSpotFree'); $response = api_client()->get('/auth/session', $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()) ->toBeArray() ->toHaveKey('customer_number', $session['user']['customer_number']) ->and($response->data()['runtime_config']['economic']['transaction_draft_customer_number'] ?? null) ->toBe(556677) ->and($response->data()['runtime_config']['economic']['default_distribution_department_id'] ?? null) ->toBe(65) ->and($response->data()['permissions'] ?? []) ->toContain('has_attribute_restrictSpotFree'); }); it('rejects invalid auth session tokens', function (): void { api_test_covers('GET /auth/session', 'auth'); $response = api_client()->get('/auth/session', [ 'Authorization' => 'Bearer invalid-token', ]); $response ->assertStatus(401) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid token'); }); it('logs out and invalidates the token for future session calls', function (): void { api_test_covers('GET /auth/logout', 'happy'); $session = api_fixtures()->createUserSession(['list_departments'], [ 'display_name' => 'Logout User', ]); api_fixtures()->cacheAuthSessionForUser( $session['user'], $session['token'], ['list_departments'] ); $logoutResponse = api_client()->get('/auth/logout', $session['headers']); $logoutResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Logged out'); $followUpSession = api_client()->get('/auth/session', $session['headers']); $followUpSession ->assertStatus(401) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid token'); }); it('logs out and invalidates cached subuser sessions', function (): void { api_test_covers('GET /auth/logout', 'happy'); $user = api_fixtures()->createUser(); $session = api_fixtures()->createSubuserSession((int)$user['customer_number'], [], [ 'username' => 'logout-driver', ]); $warmCache = api_client()->get('/subusers/me', $session['headers']); $warmCache ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $logoutResponse = api_client()->get('/auth/logout', $session['headers']); $logoutResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess() ->assertMessage('Logged out'); $followUpProfile = api_client()->get('/subusers/me', $session['headers']); $followUpProfile ->assertStatus(401) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Unauthorized'); }); it('scopes passkey challenges to the requested principal type', function (): void { api_test_covers('POST /auth/passkey/challenge', 'auth'); api_fixtures()->setModuleConfig('reCAPTCHA', 'enabled', 'false'); $user = api_fixtures()->createUser([ 'display_name' => 'Passkey Customer', ]); $subuser = api_fixtures()->createSubuser([ 'username' => 'passkey-driver', ]); api_fixtures()->createPasskey([ 'user_id' => (int)$user['id'], 'is_subuser' => false, 'credential_id' => 'customer-passkey-credential', ]); api_fixtures()->createPasskey([ 'user_id' => (int)$subuser['id'], 'is_subuser' => true, 'credential_id' => 'subuser-passkey-credential', ]); $subuserChallenge = api_client()->post('/auth/passkey/challenge', [ 'principal_type' => 'subuser', 'username' => 'passkey-driver', ]); $userChallenge = api_client()->post('/auth/passkey/challenge', [ 'principal_type' => 'user', 'customer_number' => (int)$user['customer_number'], ]); $subuserChallenge ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $userChallenge ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $subuserCredentials = $subuserChallenge->data()['publicKey']['allowCredentials'] ?? []; $userCredentials = $userChallenge->data()['publicKey']['allowCredentials'] ?? []; $subuserCredentialIds = array_column($subuserCredentials, 'id'); $userCredentialIds = array_column($userCredentials, 'id'); expect($subuserCredentialIds)->toContain('subuser-passkey-credential'); expect($subuserCredentialIds)->not->toContain('customer-passkey-credential'); expect($userCredentialIds)->toContain('customer-passkey-credential'); expect($userCredentialIds)->not->toContain('subuser-passkey-credential'); $subuserChallengeToken = (string)($subuserChallenge->data()['challenge_token'] ?? ''); $userChallengeToken = (string)($userChallenge->data()['challenge_token'] ?? ''); if ($subuserChallengeToken !== '') { api_fixtures()->cleanupDeleteWhere('tokens', ['token' => $subuserChallengeToken]); } if ($userChallengeToken !== '') { api_fixtures()->cleanupDeleteWhere('tokens', ['token' => $userChallengeToken]); } }); it('rejects invalid logout tokens', function (): void { api_test_covers('GET /auth/logout', 'auth'); $response = api_client()->get('/auth/logout', [ 'Authorization' => 'Bearer invalid-token', ]); $response ->assertStatus(401) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Invalid token'); });