Skip to content
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
85 changes: 85 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Security review: this workflow uses no untrusted inputs (no github.event.issue.*,
# pull_request.*body/title, head_commit.message, etc.). All ${{ ... }} expansions
# are static (github.workflow, github.ref, matrix.php) or pulled from secrets via
# env, never interpolated into shell commands.
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
unit-tests:
name: Unit tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.4']
steps:
- uses: actions/checkout@v4

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
tools: composer:v2

- name: Install dependencies
uses: ramsey/composer-install@v3
with:
composer-options: '--no-interaction --no-progress --prefer-dist'

- name: Run unit tests
run: vendor/bin/phpunit --testsuite unit

# Integration tests hit the real TransUnion ShareAble sandbox API and require
# credentials. They run only on manual maintainer trigger so PR validation
# never burns sandbox quota and so secrets are never exposed to fork PRs.
#
# Required repository secrets (Settings → Secrets and variables → Actions):
# TU_SHAREABLE_URL, TU_SHAREABLE_CLIENT_ID,
# TU_SHAREABLE_API_KEY_ONE, TU_SHAREABLE_API_KEY_TWO
integration-tests:
name: Integration tests (TU sandbox)
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2

- name: Install dependencies
uses: ramsey/composer-install@v3
with:
composer-options: '--no-interaction --no-progress --prefer-dist'

- name: Write sandbox config
env:
TU_URL: ${{ secrets.TU_SHAREABLE_URL }}
TU_CLIENT_ID: ${{ secrets.TU_SHAREABLE_CLIENT_ID }}
TU_API_KEY_ONE: ${{ secrets.TU_SHAREABLE_API_KEY_ONE }}
TU_API_KEY_TWO: ${{ secrets.TU_SHAREABLE_API_KEY_TWO }}
run: |
{
printf 'url="%s"\n' "$TU_URL"
printf 'clientId="%s"\n' "$TU_CLIENT_ID"
printf 'apiKeyOne="%s"\n' "$TU_API_KEY_ONE"
printf 'apiKeyTwo="%s"\n' "$TU_API_KEY_TWO"
} > config

- name: Run integration tests
run: vendor/bin/phpunit --testsuite integration
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"repositories": [
{
"type": "vcs",
"url": "git@github.com:rentpost/coding-standard-php.git"
"url": "https://github.com/rentpost/coding-standard-php.git"
}
],
"autoload": {
Expand Down
23 changes: 8 additions & 15 deletions src/Model/Attestation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Rentpost\TUShareable\Model;

use Symfony\Component\Validator\Constraints as Assert;

/**
* Class that represents a single attestation.
*/
Expand All @@ -18,39 +16,34 @@ class Attestation
public function __construct(
private int $attestationId,
private int $attestationTypeId,

#[Assert\NotBlank]
private string $name,

#[Assert\NotBlank]
private string $legalText,

private ?string $name,
private ?string $legalText,
private bool $affirmativeRequired,
private string $additionalInformation,
private ?string $additionalInformation,
) {
$this->validate();
}


public function getAttestationId(): ?int
public function getAttestationId(): int
{
return $this->attestationId;
}


public function getAttestationTypeId(): ?int
public function getAttestationTypeId(): int
{
return $this->attestationTypeId;
}


public function getName(): string
public function getName(): ?string
{
return $this->name;
}


public function getLegalText(): string
public function getLegalText(): ?string
{
return $this->legalText;
}
Expand All @@ -62,7 +55,7 @@ public function isAffirmativeRequired(): bool
}


public function getAdditionalInformation(): string
public function getAdditionalInformation(): ?string
{
return $this->additionalInformation;
}
Expand Down
14 changes: 7 additions & 7 deletions src/Model/AttestationGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,20 @@ public function addAttestationResponse(int $attestationId, bool $isAffirmative):
}


/** @param array<string, int|array<string, mixed>> $data */
/** @param array<string, int|array<string, mixed>|null> $data */
public static function fromArray(array $data): self
{
$attestationGroupId = $data['attestationGroupId'];

$attestations = [];
foreach ($data['attestations'] as $attestation) {
foreach (($data['attestations'] ?? []) as $attestation) {
$attestations[] = new Attestation(
$attestation['attestationId'],
$attestation['attestationTypeId'],
$attestation['name'],
$attestation['legalText'],
$attestation['name'] ?? null,
$attestation['legalText'] ?? null,
$attestation['affirmativeRequired'],
$attestation['additionalInformation'],
$attestation['additionalInformation'] ?? null,
);
}

Expand All @@ -83,11 +83,11 @@ public function toArray(): array
'attestationGroupId' => $this->attestationGroupId,
];

foreach ($this->attestations as $attestation) {
foreach (($this->attestations ?? []) as $attestation) {
$array['attestations'][] = $attestation->toArray();
}

foreach ($this->attestationResponses as $attestationResponse) {
foreach (($this->attestationResponses ?? []) as $attestationResponse) {
$array['attestationResponses'][] = $attestationResponse->toArray();
}

Expand Down
129 changes: 129 additions & 0 deletions test/Unit/Model/AttestationGroupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types = 1);

namespace Test\Unit\Rentpost\TUShareable\Model;

use PHPUnit\Framework\TestCase;
use Rentpost\TUShareable\Model\Attestation;
use Rentpost\TUShareable\Model\AttestationGroup;

class AttestationGroupTest extends TestCase
{

public function testConstructorWithAttestations(): void
{
$attestation = new Attestation(1, 2, 'Name', 'Legal Text', true, 'Info');
$group = new AttestationGroup(123, [$attestation]);

$this->assertSame(123, $group->getAttestationGroupId());
$this->assertCount(1, $group->getAttestations());
$this->assertNull($group->getAttestationsResponses());
}


public function testConstructorWithoutAttestations(): void
{
$group = new AttestationGroup(123);

$this->assertSame(123, $group->getAttestationGroupId());
$this->assertNull($group->getAttestations());
}


public function testFromArrayWithAttestations(): void
{
$group = AttestationGroup::fromArray([
'attestationGroupId' => 123,
'attestations' => [
[
'attestationId' => 1,
'attestationTypeId' => 2,
'name' => 'CA ICRAA',
'legalText' => 'I certify...',
'affirmativeRequired' => true,
'additionalInformation' => null,
],
],
]);

$this->assertSame(123, $group->getAttestationGroupId());
$this->assertCount(1, $group->getAttestations());
}


public function testFromArrayWithMissingAttestationsKey(): void
{
$group = AttestationGroup::fromArray(['attestationGroupId' => 123]);

$this->assertSame(123, $group->getAttestationGroupId());
$this->assertSame([], $group->getAttestations());
}


public function testFromArrayWithNullAttestations(): void
{
$group = AttestationGroup::fromArray([
'attestationGroupId' => 123,
'attestations' => null,
]);

$this->assertSame(123, $group->getAttestationGroupId());
$this->assertSame([], $group->getAttestations());
}


public function testFromArrayWithNullableAttestationFields(): void
{
$group = AttestationGroup::fromArray([
'attestationGroupId' => 123,
'attestations' => [
[
'attestationId' => 1,
'attestationTypeId' => 1,
'affirmativeRequired' => false,
// name, legalText, additionalInformation all absent
],
],
]);

$attestation = $group->getAttestations()[0];
$this->assertNull($attestation->getName());
$this->assertNull($attestation->getLegalText());
$this->assertNull($attestation->getAdditionalInformation());
}


public function testToArrayWithoutAttestationsOrResponses(): void
{
$group = new AttestationGroup(123);

$this->assertSame(['attestationGroupId' => 123], $group->toArray());
}


public function testToArrayWithAttestationsAndResponses(): void
{
$attestation = new Attestation(1, 2, 'Name', 'Legal', true, null);
$group = new AttestationGroup(123, [$attestation]);
$group->addAttestationResponse(1, true);

$array = $group->toArray();

$this->assertSame(123, $array['attestationGroupId']);
$this->assertCount(1, $array['attestations']);
$this->assertCount(1, $array['attestationResponses']);
$this->assertSame(['attestationId' => 1, 'isAffirmative' => true], $array['attestationResponses'][0]);
}


public function testAddAttestationResponseWithFalseAffirmation(): void
{
$group = new AttestationGroup(123);
$group->addAttestationResponse(7, false);

$responses = $group->getAttestationsResponses();
$this->assertCount(1, $responses);
$this->assertFalse($responses[0]->isAffirmative());
}
}
38 changes: 16 additions & 22 deletions test/Unit/Model/AttestationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

namespace Test\Unit\Rentpost\TUShareable\Model;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Rentpost\TUShareable\Model\Attestation;
use Rentpost\TUShareable\ValidationException;

class AttestationTest extends TestCase
{
Expand Down Expand Up @@ -42,29 +40,25 @@ public function testConstructorAndGetters(): void


/**
* @return array<array<string, string>>
* Per the TU API spec, name, legalText, and additionalInformation are nullable.
* Construction must not throw when any (or all) are null.
*/
public static function validationProvider(): array
public function testNullableStringFields(): void
{
return [
// name missing
[ [1, 2, '', 'Legal Text', true, 'Additional Information'], 'name', 'This value should not be blank.' ],
// legal text missing
[ [1, 2, 'Name', '', true, 'Additional Information'], 'legalText', 'This value should not be blank.' ],
];
}

$attestation = new Attestation(1, 2, null, null, false, null);

/**
* @param string[] $values
*/
#[DataProvider('validationProvider')]
public function testValidationErrors(array $values, string $field, string $errorMessage): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Object(Rentpost\TUShareable\Model\Attestation).$field");
$this->expectExceptionMessage($errorMessage);
$this->assertNull($attestation->getName());
$this->assertNull($attestation->getLegalText());
$this->assertNull($attestation->getAdditionalInformation());
$this->assertFalse($attestation->isAffirmativeRequired());

new Attestation(...$values);
$this->assertSame([
'attestationId' => 1,
'attestationTypeId' => 2,
'name' => null,
'legalText' => null,
'affirmativeRequired' => false,
'additionalInformation' => null,
], $attestation->toArray());
}
}
Loading