Skip to content

Commit

Permalink
Finish test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
oddevan committed Jan 20, 2024
1 parent 4fd2aa8 commit 4116088
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 40 deletions.
3 changes: 1 addition & 2 deletions src/ActivityPub/MessageSender.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ public function send(
$this->log->error($errorMessage, [
'message' => $message->toArray(),
'inbox' => $toInbox,
'key ID' => $withKeyId,
'key PEM present' => empty($signedWithPrivateKey) ? 'absent' : 'present',
'signed' => $request->hasHeader('signature') ? "With key $withKeyId" : 'NO',
]);
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/ActivityPub/Signatures/MessageVerifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ public function verifyDigest(RequestInterface $request): bool {
$headerLine = $request->getHeaderLine('digest');

$equalPosition = strpos($headerLine, '=');
$expected = substr($headerLine, $equalPosition === false ? 0 : $equalPosition + 1);
if ($equalPosition === false) {
// No '=' means this isn't even base64 encoded much less contains the sha-256 opener. Bail out!
return false;
}

$expected = substr($headerLine, $equalPosition + 1);
$actual = base64_encode(hash('sha256', $request->getBody()->__toString(), true));

return $expected === $actual;
Expand Down
9 changes: 2 additions & 7 deletions src/Infrastructure/DefaultMessageBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Smolblog\Framework\Messages\Message;
use Smolblog\Framework\Messages\MessageBus;
use Smolblog\Framework\Messages\Query;
Expand All @@ -32,7 +33,7 @@ class DefaultMessageBus implements MessageBus {
*/
public function __construct(
ListenerProviderInterface $provider,
private LoggerInterface $log,
private LoggerInterface $log = new NullLogger(),
) {
$this->internal = new Dispatcher($provider, $log);
}
Expand All @@ -44,10 +45,6 @@ public function __construct(
* @return mixed Message potentially modified by listeners.
*/
public function dispatch(object $message): mixed {
$this->log->debug(
'Dispatching message ' . get_class($message),
method_exists($message, 'toArray') ? $message->toArray() : get_object_vars($message),
);
return $this->internal->dispatch($message);
}

Expand All @@ -58,7 +55,6 @@ public function dispatch(object $message): mixed {
* @return mixed Results of the query.
*/
public function fetch(Query $query): mixed {
$this->log->debug('Fetching query ' . get_class($query), $query->toArray());
return $this->internal->dispatch($query)->results();
}

Expand All @@ -73,7 +69,6 @@ public function fetch(Query $query): mixed {
* @return void
*/
public function dispatchAsync(Message $message): void {
$this->log->debug('Dispatching async message ' . get_class($message), $message->toArray());
$this->internal->dispatch(new AsyncWrappedMessage($message));
}
}
15 changes: 9 additions & 6 deletions test-utils/CoverageReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ public static function report() {
$report = require __DIR__ . '/../coverage.php';
$baseDirCharCount = strlen(dirname(__DIR__) . '/src/');

$score = $report->getReport()->numberOfExecutedPaths() / $report->getReport()->numberOfExecutablePaths();
$score = $report->getReport()->numberOfExecutedBranches() / $report->getReport()->numberOfExecutableBranches();

echo 'Total coverage: ' . floor($score * 100) . "%\n";
echo "Expected branch coverage: 100%\n";
echo ' Actual branch coverage: ' . floor($score * 100) . "%\n";

if ($score >= 1) {
echo "PASS\n\n";
return;
}

echo "FAIL\n\nThe following files have incomplete code coverage:\n";
echo "\nThe following files have incomplete branch coverage:\n";
foreach(self::getProblemFiles($report->getReport()) as $file) {
echo ' ' . substr($file->pathAsString(), $baseDirCharCount) . "\n";
}
Expand All @@ -35,7 +36,7 @@ private static function getProblemFiles(Directory $dir): array {
fn($all, $subDir) => array_merge($all, self::getProblemFiles($subDir)),
array_filter(
$dir->files(),
fn($file) => ($file->numberOfExecutablePaths() - $file->numberOfExecutedPaths()) > 0
fn($file) => ($file->numberOfExecutableBranches() - $file->numberOfExecutedBranches()) > 0
),
);
}
Expand All @@ -44,7 +45,8 @@ private static function getFileMessage(File $file): string {
$output = $file->pathAsString() . ":\n";

foreach (array_filter($file->functions(), fn($fn) => $fn['coverage'] < 100) as $func) {
$output .= ' Function ' . $func['functionName'] . ': ' . ($func['executablePaths'] - $func['executedPaths']) . "\n";
$output .= ' Function ' . $func['functionName'] . ': ' . ($func['executable
Branches'] - $func['executedBranches']) . "\n";
}
foreach (array_filter($file->traits(), fn($fn) => $fn['coverage'] < 100) as $trait) {
$output .= ' Trait ' . $trait['traitName'] . ":\n" . self::getMethodMessages($trait['methods']);
Expand All @@ -60,7 +62,8 @@ private static function getFileMessage(File $file): string {
private static function getMethodMessages(array $methods): string {
$output = '';
foreach (array_filter($methods, fn($mt) => $mt['coverage'] < 100) as $method) {
$output .= ' ' . $method['methodName'] . ': ' . ($method['executablePaths'] - $method['executedPaths']) . "\n";
$output .= ' ' . $method['methodName'] . ': ' . ($method['executable
Branches'] - $method['executedBranches']) . "\n";
}

return $output;
Expand Down
38 changes: 34 additions & 4 deletions tests/ActivityPub/MessageSenderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function testItWillSendTheMessageAndLogTheAttempt() {
);
}

public function testItLogsRemoteServerErrorsByDefault() {
public function testItLogsRemoteServerErrorsByDefaultAndGivesTheSigningKeyId() {
$actorId = '//smol.blog/' . $this->randomId() . '/actor.json';
$object = new Accept(
id: '//smol.blog/outbox/' . $this->randomId(),
Expand All @@ -117,7 +117,7 @@ public function testItLogsRemoteServerErrorsByDefault() {
),
);

$this->signer->method('sign')->willReturnArgument(0);
$this->signer->method('sign')->willReturnCallback(fn($req) => $req->withAddedHeader('Signature', 'Me!'));
$this->httpClient->method('sendRequest')->willReturn(
new HttpResponse(code: 404, body: ['error' => 'inbox does not exist'])
);
Expand All @@ -127,8 +127,7 @@ public function testItLogsRemoteServerErrorsByDefault() {
[
'message' => $object->toArray(),
'inbox' => 'https://smol.blog/inbox',
'key ID' => "$actorId#publicKey",
'key PEM present' => 'present',
'signed' => "With key $actorId#publicKey",
]
);

Expand All @@ -140,6 +139,37 @@ public function testItLogsRemoteServerErrorsByDefault() {
);
}

public function testItLogsRemoteServerErrorsAndNotesTheLackOfSignature() {
$actorId = '//smol.blog/' . $this->randomId() . '/actor.json';
$object = new Accept(
id: '//smol.blog/outbox/' . $this->randomId(),
actor: $actorId,
object: new Follow(
id: '//smol.blog/outbox/' . $this->randomId(),
actor: '//smol.blog/' . $this->randomId() . '/actor.json',
object: $actorId,
),
);

$this->httpClient->method('sendRequest')->willReturn(
new HttpResponse(code: 404, body: ['error' => 'inbox does not exist'])
);

$this->logger->expects($this->once())->method('error')->with(
'Error from federated server: {"error":"inbox does not exist"}',
[
'message' => $object->toArray(),
'inbox' => 'https://smol.blog/inbox',
'signed' => 'NO',
]
);

$this->subject->send(
message: $object,
toInbox: 'https://smol.blog/inbox',
);
}

public function testItCanBeConfiguredToThrowExceptionsOnRemoteServerErrors() {
$actorId = '//smol.blog/' . $this->randomId() . '/actor.json';
$object = new Accept(
Expand Down
37 changes: 37 additions & 0 deletions tests/ActivityPub/Objects/ActivityPubBaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,41 @@ public function testItOnlyRequiresAnId() {
public function testAdditionalPropertiesAreAccessable() {
$this->assertEquals('hullo', $this->subject->randomProperty);
}

public function testItDeserializesAnObjectWithNoTypeToNull() {
$this->assertNull(ActivityPubBase::typedObjectFromArray(['id' => 'https://smol.blog/actor']));
}

public function testItDeserializesAnObjectOfUnknownTypeToNull() {
$this->assertNull(ActivityPubBase::typedObjectFromArray(['id' => 'https://smol.blog/actor', 'type' => 'Snek']));
}

public function testItDeserializesAnObjectWithTypeObjectToActivityPubObject() {
$this->assertInstanceOf(
ActivityPubObject::class,
ActivityPubBase::typedObjectFromArray([
'id' => 'https://smol.blog/actor',
'type' => 'Object',
])
);
}

public function testItDeserializesAnObjectWithAnActorTypeToActor() {
foreach (ActorType::cases() as $type) {
$this->assertInstanceOf(Actor::class, ActivityPubBase::typedObjectFromArray([
'id' => 'https://smol.blog/actor',
'type' => $type->value,
]), "Did not deserialize ActorType $type->value correctly.");
}
}

public function testItDeserializesAnObjectWithAKnownTypeToThatObject() {
$this->assertInstanceOf(
Note::class,
ActivityPubBase::typedObjectFromArray([
'id' => 'https://smol.blog/thing',
'type' => 'Note',
])
);
}
}
14 changes: 14 additions & 0 deletions tests/ActivityPub/Signatures/MessageVerifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ public function testItReturnsTrueIfThereIsNoBodyAndNoDigest() {
$this->assertTrue($this->subject->verifyDigest($request, $this->publicKeyPem));
}

public function testItReturnsFalseIfTheDigestHeaderHasNoEqualsSign() {
$request = new HttpRequest(
verb: HttpVerb::POST,
url: 'https://myhost.example/path/to/resource',
headers: [
'date' => 'Wed, 15 Mar 2023 17:28:15 GMT',
'digest' => 'VOV9b4OFUAdF0mGBVK62bE+PT3t0UtTEfq7hNT3zv9U',
],
body: '{"cows": "are the best"}',
);

$this->assertFalse($this->subject->verifyDigest($request));
}

public function testItRejectsMessagesOlderThanTwentyFourHours() {
$request = new HttpRequest(
verb: HttpVerb::POST,
Expand Down
41 changes: 41 additions & 0 deletions tests/Exceptions/ServiceRegistryConfigurationExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Smolblog\Framework\Exceptions;

use Exception;
use Smolblog\Framework\Infrastructure\KeypairGenerator;
use Smolblog\Test\TestCase;

final class ServiceRegistryConfigurationExceptionTest extends TestCase {
public function testItUsesTheSuppliedMessageIfOneIsGiven() {
$ex = new ServiceRegistryConfigurationException(
service: KeypairGenerator::class,
config: [],
message: 'Something happened.'
);

$this->assertEquals('Something happened.', $ex->getMessage());
}

public function testItGeneratesADefaultMessageIfNeeded() {
$ex = new ServiceRegistryConfigurationException(
service: KeypairGenerator::class,
config: [],
);

$this->assertEquals('Configuration error for '.KeypairGenerator::class.' in ServiceRegistry.', $ex->getMessage());
}

public function testItGeneratesADefaultMessageUsingThePreviousException() {
$ex = new ServiceRegistryConfigurationException(
service: KeypairGenerator::class,
config: [],
previous: new Exception('There was a problem.')
);

$this->assertEquals(
'Configuration error for '.KeypairGenerator::class.' in ServiceRegistry: There was a problem.',
$ex->getMessage()
);
}
}
39 changes: 19 additions & 20 deletions tests/Infrastructure/DefaultMessageBusTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,62 @@
use Smolblog\Test\TestCase;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Smolblog\Framework\Messages\Command;
use Smolblog\Framework\Messages\Query;
use Smolblog\Framework\Objects\Identifier;

final class DefaultMessageBusTest extends TestCase {
private ListenerProviderInterface $provider;

protected function setUp(): void {
$this->provider = $this->createMock(ListenerProviderInterface::class);

$this->subject = new DefaultMessageBus(
provider: $this->provider,
);
}

public function testItCallsListenersInOrder() {
$providerStub = $this->createStub(ListenerProviderInterface::class);
$providerStub->method('getListenersForEvent')->willReturn([
$this->provider->method('getListenersForEvent')->willReturn([
fn($event) => $event->trace[] = 'first',
fn($event) => $event->trace[] = 'second',
fn($event) => $event->trace[] = 'third',
]);

$bus = new DefaultMessageBus($providerStub, new NullLogger());

$message = new class() { public $trace = []; };
$bus->dispatch($message);
$this->subject->dispatch($message);

$this->assertEquals(['first','second','third'], $message->trace);
}

public function testItStopsCallingListenersWhenEventStopped() {
$providerStub = $this->createStub(ListenerProviderInterface::class);
$providerStub->method('getListenersForEvent')->willReturn([
$this->provider->method('getListenersForEvent')->willReturn([
fn($event) => $event->trace[] = 'first',
fn($event) => $event->active = false,
fn($event) => $event->trace[] = 'third',
]);

$bus = new DefaultMessageBus($providerStub, new NullLogger());

$message = new class() implements StoppableEventInterface {
public $trace = [];
public $active = true;
public function isPropagationStopped(): bool { return !$this->active; }
};
$bus->dispatch($message);
$this->subject->dispatch($message);

$this->assertEquals(['first'], $message->trace);
}

public function testItUnwrapsAQueryWhenFetched() {
$expected = '59cb2796-411c-4f3d-89f2-07dae78787e6';

$providerStub = $this->createStub(ListenerProviderInterface::class);
$providerStub->method('getListenersForEvent')->willReturn([
$this->provider->method('getListenersForEvent')->willReturn([
fn($event) => $event->setResults($expected),
]);

$bus = new DefaultMessageBus($providerStub, new NullLogger());

$message = new class() extends Query {};
$actual = $bus->fetch($message);
$actual = $this->subject->fetch($message);

$this->assertEquals($expected, $actual);
}
Expand All @@ -67,13 +69,10 @@ public function testItCanWrapAMessageInAnAsyncMessageWrapper() {
$message = new class($this->randomId()) extends Command { public function __construct(public readonly Identifier $thing) {} };
$asyncMessage = new AsyncWrappedMessage($message);

$providerStub = $this->createStub(ListenerProviderInterface::class);
$providerStub->method('getListenersForEvent')->willReturn([
$this->provider->method('getListenersForEvent')->willReturn([
fn($event) => $this->assertEquals($asyncMessage, $event),
]);

$bus = new DefaultMessageBus($providerStub, new NullLogger());

$bus->dispatchAsync($message);
$this->subject->dispatchAsync($message);
}
}
7 changes: 7 additions & 0 deletions tests/Objects/IdentifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ public function testAnIdentifierCanBeMadeFromABinaryString() {

$this->assertEquals('1d1413ca-33d8-4c2d-8029-ea41e38654cf', $actual->toString());
}

public function testTheJsonRepresentationIsJustTheString() {
$expected = 'fb0914b3-0224-4150-bd4b-2934aaddf9be';
$actual = Identifier::fromString($expected);

$this->assertEquals("\"$expected\"", json_encode($actual));
}
}

0 comments on commit 4116088

Please sign in to comment.