Skip to content

Commit

Permalink
Merge branch 'bss-48' into development
Browse files Browse the repository at this point in the history
[BSS-48]: Implement observer pattern for enhanced client-request synchronization
  • Loading branch information
shahmal1yev committed Sep 21, 2024
2 parents 6faad89 + 40242bc commit c77ca94
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 6 deletions.
3 changes: 2 additions & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

use Atproto\Traits\Authentication;
use Atproto\Traits\Smith;
use SplSubject;

class Client
class Client implements SplSubject
{
use Smith;
use Authentication;
Expand Down
3 changes: 2 additions & 1 deletion src/Contracts/HTTP/APIRequestContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace Atproto\Contracts\HTTP;

use Atproto\Contracts\RequestContract;
use SplObserver;

interface APIRequestContract extends RequestContract
interface APIRequestContract extends RequestContract, SplObserver
{
public const API_BASE_URL = 'https://bsky.social';
public const API_BASE_HEADERS = [
Expand Down
11 changes: 9 additions & 2 deletions src/HTTP/API/APIRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
use Atproto\Contracts\HTTP\APIRequestContract;
use Atproto\Contracts\HTTP\Resources\ResourceContract;
use Atproto\HTTP\Request;
use SplSubject;

abstract class APIRequest extends Request implements APIRequestContract
{
private Client $client;
protected Client $client;

public function __construct(Client $client)
{
Expand All @@ -22,7 +23,7 @@ public function send(): ResourceContract
return $this->resource(parent::send());
}

private function initialize(): void
protected function initialize(): void
{
$this->origin(self::API_BASE_URL)
->path($this->endpoint())
Expand All @@ -42,4 +43,10 @@ private function endpoint(): string
}

abstract public function resource(array $data): ResourceContract;

public function update(SplSubject $subject): void
{
/** @var Client $subject */
$this->client = $subject;
}
}
8 changes: 8 additions & 0 deletions src/HTTP/Traits/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Atproto\Client;
use Atproto\HTTP\API\APIRequest;
use SplSubject;

trait Authentication
{
Expand All @@ -14,6 +15,13 @@ public function __construct(Client $client)
}

parent::__construct($client);
$this->update($client);
}

public function update(SplSubject $client): void
{
/** @var Client $client */
parent::update($client);

if ($authenticated = $client->authenticated()) {
$this->header("Authorization", "Bearer " . $authenticated->accessJwt());
Expand Down
27 changes: 27 additions & 0 deletions src/Traits/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@

use Atproto\Exceptions\BlueskyException;
use Atproto\Resources\Com\Atproto\Server\CreateSessionResource;
use SplObjectStorage;
use SplObserver;

trait Authentication
{
private ?CreateSessionResource $authenticated = null;
private SplObjectStorage $observers;

public function __construct()
{
$this->observers = new SplObjectStorage();
}

/**
* @throws BlueskyException
Expand All @@ -20,10 +28,29 @@ public function authenticate(string $identifier, string $password): void
$response = $request->send();

$this->authenticated = $response;

$this->notify();
}

public function authenticated(): ?CreateSessionResource
{
return $this->authenticated;
}

public function attach(SplObserver $observer)
{
$this->observers->attach($observer);
}

public function detach(SplObserver $observer)
{
$this->observers->detach($observer);
}

public function notify()
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
6 changes: 6 additions & 0 deletions src/Traits/Smith.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Atproto\Traits;

use Atproto\Client;
use Atproto\Contracts\HTTP\APIRequestContract;
use Atproto\Contracts\Observer;
use Atproto\Contracts\RequestContract;
use Atproto\Exceptions\Http\Request\RequestNotFoundException;

Expand Down Expand Up @@ -31,8 +33,10 @@ public function forge(...$arguments): RequestContract
throw new RequestNotFoundException("$request class does not exist.");
}

/** @var APIRequestContract $request */
$request = new $request(...$arguments);

$this->attach($request);
$this->refresh();

return $request;
Expand All @@ -55,4 +59,6 @@ private function request(): string
{
return $this->prefix . $this->path();
}


}
24 changes: 24 additions & 0 deletions tests/Feature/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,28 @@ public function testClientThrowsExceptionWhenAuthenticationRequired(): void
->forge()
->send();
}

/**
* @throws BlueskyException
*/
public function testObserverNotificationOnAuthentication(): void
{
$request = $this->client->app()
->bsky()
->actor()
->getProfile()
->forge();

$this->client->authenticate(
$_ENV['BLUESKY_IDENTIFIER'],
$_ENV['BLUESKY_PASSWORD']
);

$response = $request->actor($this->client->authenticated()->did())
->build()
->send();

$this->assertInstanceOf(ResourceContract::class, $response);
$this->assertInstanceOf(GetProfileResource::class, $response);
}
}
86 changes: 84 additions & 2 deletions tests/Unit/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
use Atproto\Client;
use Atproto\Contracts\RequestContract;
use Atproto\Exceptions\Http\Request\RequestNotFoundException;
use Atproto\HTTP\API\APIRequest;
use Atproto\HTTP\API\Requests\Com\Atproto\Server\CreateSession;
use Atproto\Resources\Com\Atproto\Server\CreateSessionResource;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use SplObserver;
use Tests\Supports\Reflection;

class ClientTest extends TestCase
Expand Down Expand Up @@ -48,7 +52,7 @@ public function testNamespaceGeneration(): void
$this->assertSame($expectedNamespace, $namespace);
}

public function testBuildThrowsRequestNotFoundException(): void
public function testForgeThrowsRequestNotFoundException(): void
{
$this->client->nonExistentMethod();

Expand All @@ -61,7 +65,7 @@ public function testBuildThrowsRequestNotFoundException(): void
/**
* @throws RequestNotFoundException
*/
public function testBuildReturnsRequestContract(): void
public function testForgeReturnsRequestContract(): void
{
$this->client->app()->bsky()->actor()->getProfile();

Expand Down Expand Up @@ -90,4 +94,82 @@ public function testAuthenticatedReturnsNullWhenNotAuthenticated(): void
{
$this->assertNull($this->client->authenticated());
}

public function testAttachObserver(): void
{
$mockObserver = $this->createMock(SplObserver::class);
$this->client->attach($mockObserver);

$observers = $this->getPropertyValue('observers', $this->client);
$this->assertCount(1, $observers);
$this->assertTrue($observers->contains($mockObserver));
}

public function testDetachObserver(): void
{
$mockObserver = $this->createMock(SplObserver::class);
$this->client->attach($mockObserver);
$this->client->detach($mockObserver);

$observers = $this->getPropertyValue('observers', $this->client);
$this->assertCount(0, $observers);
}

public function testNotifyObservers(): void
{
$mockObserver1 = $this->createMock(SplObserver::class);
$mockObserver2 = $this->createMock(SplObserver::class);

$mockObserver1->expects($this->once())->method('update')->with($this->client);
$mockObserver2->expects($this->once())->method('update')->with($this->client);

$this->client->attach($mockObserver1);
$this->client->attach($mockObserver2);

$this->client->notify();
}

public function testForgeAttachesObserver(): void
{
$this->client->app()->bsky()->actor()->getProfile();
$request = $this->client->forge();

$observers = $this->getPropertyValue('observers', $this->client);
$this->assertCount(1, $observers);
$this->assertTrue($observers->contains($request));
}

public function testAPIRequestUpdatesOnNotify(): void
{
$this->mockAuthenticate();

$mockRequest = $this->createMock(APIRequest::class);

$this->client->attach($mockRequest);

$mockRequest->expects($this->once())
->method('update')
->with($this->client);

$this->client->authenticate('username', 'password');
}

private function mockAuthenticate()
{
$mockCreateSession = $this->getMockBuilder(CreateSession::class)
->disableOriginalConstructor()
->onlyMethods(['send'])
->getMock();

$mockCreateSession->expects($this->once())
->method('send')
->willReturn($this->createMock(CreateSessionResource::class));

$this->client = $this->getMockBuilder(Client::class)
->onlyMethods(['forge'])
->getMock();

$this->client->method('forge')
->willReturn($mockCreateSession);
}
}

0 comments on commit c77ca94

Please sign in to comment.