From b7ddba92ac3d17f989dbf1faadf2341ddfff3fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Ignacio=20Torres?= Date: Mon, 18 May 2026 12:39:47 -0700 Subject: [PATCH] test: add missing unit tests for new classes --- tests/Application/Actions/ActionErrorTest.php | 58 ++++++++ .../Application/Actions/ActionPayloadTest.php | 70 ++++++++++ tests/Application/Settings/SettingsTest.php | 36 +++++ .../Account/AppPassword/AppPasswordTest.php | 63 +++++++++ .../Account/EmailToken/EmailTokenTest.php | 56 ++++++++ .../Account/InviteCode/InviteCodeTest.php | 55 ++++++++ .../Account/InviteCode/InviteCodeUseTest.php | 43 ++++++ .../Account/RefreshToken/RefreshTokenTest.php | 61 +++++++++ tests/Domain/Blob/BlobTest.php | 67 +++++++++ tests/Domain/Did/DidDocEntryTest.php | 43 ++++++ tests/Domain/Lexicon/LexiconEntryTest.php | 92 +++++++++++++ tests/Domain/OAuth/AccountDeviceTest.php | 46 +++++++ .../Domain/OAuth/AuthorizationRequestTest.php | 86 ++++++++++++ tests/Domain/OAuth/AuthorizedClientTest.php | 54 ++++++++ tests/Domain/OAuth/DeviceTest.php | 62 +++++++++ tests/Domain/OAuth/OAuthTokenTest.php | 101 ++++++++++++++ tests/Domain/OAuth/UsedRefreshTokenTest.php | 29 ++++ .../Identity/ResolveHandleResponseTest.php | 26 ++++ tests/Domain/Preference/AccountPrefTest.php | 39 ++++++ tests/Domain/Record/BacklinkTest.php | 39 ++++++ tests/Domain/Record/RecordBlobTest.php | 35 +++++ tests/Domain/Record/RecordTest.php | 71 ++++++++++ tests/Domain/Repo/CborBytesTest.php | 26 ++++ tests/Domain/Repo/CidLinkTest.php | 21 +++ tests/Domain/Repo/RepoBlockTest.php | 45 ++++++ tests/Domain/Repo/RepoRootTest.php | 45 ++++++ tests/Domain/Sequencer/RepoSeqEventTest.php | 77 +++++++++++ .../Lexicon/InMemoryLexiconRepositoryTest.php | 80 +++++++++++ .../InMemoryAccountDeviceRepositoryTest.php | 88 ++++++++++++ ...moryAuthorizationRequestRepositoryTest.php | 88 ++++++++++++ ...InMemoryAuthorizedClientRepositoryTest.php | 103 ++++++++++++++ .../OAuth/InMemoryDeviceRepositoryTest.php | 67 +++++++++ .../InMemoryOAuthTokenRepositoryTest.php | 128 ++++++++++++++++++ ...InMemoryUsedRefreshTokenRepositoryTest.php | 58 ++++++++ .../Record/InMemoryBacklinkRepositoryTest.php | 69 ++++++++++ .../InMemoryRecordBlobRepositoryTest.php | 62 +++++++++ 36 files changed, 2189 insertions(+) create mode 100644 tests/Application/Actions/ActionErrorTest.php create mode 100644 tests/Application/Actions/ActionPayloadTest.php create mode 100644 tests/Application/Settings/SettingsTest.php create mode 100644 tests/Domain/Account/AppPassword/AppPasswordTest.php create mode 100644 tests/Domain/Account/EmailToken/EmailTokenTest.php create mode 100644 tests/Domain/Account/InviteCode/InviteCodeTest.php create mode 100644 tests/Domain/Account/InviteCode/InviteCodeUseTest.php create mode 100644 tests/Domain/Account/RefreshToken/RefreshTokenTest.php create mode 100644 tests/Domain/Blob/BlobTest.php create mode 100644 tests/Domain/Did/DidDocEntryTest.php create mode 100644 tests/Domain/Lexicon/LexiconEntryTest.php create mode 100644 tests/Domain/OAuth/AccountDeviceTest.php create mode 100644 tests/Domain/OAuth/AuthorizationRequestTest.php create mode 100644 tests/Domain/OAuth/AuthorizedClientTest.php create mode 100644 tests/Domain/OAuth/DeviceTest.php create mode 100644 tests/Domain/OAuth/OAuthTokenTest.php create mode 100644 tests/Domain/OAuth/UsedRefreshTokenTest.php create mode 100644 tests/Domain/Pds/Atproto/Identity/ResolveHandleResponseTest.php create mode 100644 tests/Domain/Preference/AccountPrefTest.php create mode 100644 tests/Domain/Record/BacklinkTest.php create mode 100644 tests/Domain/Record/RecordBlobTest.php create mode 100644 tests/Domain/Record/RecordTest.php create mode 100644 tests/Domain/Repo/CborBytesTest.php create mode 100644 tests/Domain/Repo/CidLinkTest.php create mode 100644 tests/Domain/Repo/RepoBlockTest.php create mode 100644 tests/Domain/Repo/RepoRootTest.php create mode 100644 tests/Domain/Sequencer/RepoSeqEventTest.php create mode 100644 tests/Infrastructure/Persistence/Lexicon/InMemoryLexiconRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryAccountDeviceRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizationRequestRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizedClientRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryDeviceRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryOAuthTokenRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/OAuth/InMemoryUsedRefreshTokenRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/Record/InMemoryBacklinkRepositoryTest.php create mode 100644 tests/Infrastructure/Persistence/Record/InMemoryRecordBlobRepositoryTest.php diff --git a/tests/Application/Actions/ActionErrorTest.php b/tests/Application/Actions/ActionErrorTest.php new file mode 100644 index 0000000..3c20267 --- /dev/null +++ b/tests/Application/Actions/ActionErrorTest.php @@ -0,0 +1,58 @@ +assertSame(ActionError::BAD_REQUEST, $error->getType()); + $this->assertSame('Invalid input', $error->getDescription()); + } + + public function testDescriptionDefaultsToNull(): void + { + $error = new ActionError(ActionError::SERVER_ERROR); + + $this->assertNull($error->getDescription()); + } + + public function testSettersAreFluentAndUpdateValues(): void + { + $error = new ActionError(ActionError::SERVER_ERROR); + + $result = $error + ->setType(ActionError::VALIDATION_ERROR) + ->setDescription('Bad field'); + + $this->assertSame($error, $result); + $this->assertSame(ActionError::VALIDATION_ERROR, $error->getType()); + $this->assertSame('Bad field', $error->getDescription()); + } + + public function testSetDescriptionAcceptsNull(): void + { + $error = new ActionError(ActionError::SERVER_ERROR, 'oops'); + + $error->setDescription(); + + $this->assertNull($error->getDescription()); + } + + public function testJsonSerialize(): void + { + $error = new ActionError(ActionError::NOT_ALLOWED, 'nope'); + + $this->assertSame( + ['type' => ActionError::NOT_ALLOWED, 'description' => 'nope'], + $error->jsonSerialize() + ); + } +} diff --git a/tests/Application/Actions/ActionPayloadTest.php b/tests/Application/Actions/ActionPayloadTest.php new file mode 100644 index 0000000..2fe9a84 --- /dev/null +++ b/tests/Application/Actions/ActionPayloadTest.php @@ -0,0 +1,70 @@ +assertSame(200, $payload->getStatusCode()); + $this->assertNull($payload->getData()); + $this->assertNull($payload->getError()); + } + + public function testGettersWithAllFields(): void + { + $error = new ActionError(ActionError::BAD_REQUEST, 'bad'); + $data = ['hello' => 'world']; + + $payload = new ActionPayload(400, $data, $error); + + $this->assertSame(400, $payload->getStatusCode()); + $this->assertSame($data, $payload->getData()); + $this->assertSame($error, $payload->getError()); + } + + public function testJsonSerializeWithArrayDataIncludesStatusAndData(): void + { + $payload = new ActionPayload(201, ['id' => 1]); + + $this->assertSame( + ['statusCode' => 201, 'data' => ['id' => 1]], + $payload->jsonSerialize() + ); + } + + public function testJsonSerializeOmitsNullDataAndIncludesError(): void + { + $error = new ActionError(ActionError::SERVER_ERROR, 'kaboom'); + $payload = new ActionPayload(500, null, $error); + + $this->assertSame( + ['statusCode' => 500, 'error' => $error], + $payload->jsonSerialize() + ); + } + + public function testJsonSerializeDelegatesToJsonSerializableData(): void + { + $data = new class implements JsonSerializable { + /** @return array */ + public function jsonSerialize(): array + { + return ['delegated' => 'yes']; + } + }; + + $payload = new ActionPayload(200, $data); + + $this->assertSame(['delegated' => 'yes'], $payload->jsonSerialize()); + } +} diff --git a/tests/Application/Settings/SettingsTest.php b/tests/Application/Settings/SettingsTest.php new file mode 100644 index 0000000..0b3357d --- /dev/null +++ b/tests/Application/Settings/SettingsTest.php @@ -0,0 +1,36 @@ + 'bar', 'baz' => 42]; + $settings = new Settings($values); + + $this->assertSame($values, $settings->get()); + } + + public function testGetWithKeyReturnsValue(): void + { + $settings = new Settings(['foo' => 'bar', 'baz' => 42]); + + $this->assertSame('bar', $settings->get('foo')); + $this->assertSame(42, $settings->get('baz')); + } + + public function testGetSupportsNestedAndNullValues(): void + { + $nested = ['a' => ['b' => 'c']]; + $settings = new Settings(['nested' => $nested, 'nullable' => null]); + + $this->assertSame($nested, $settings->get('nested')); + $this->assertNull($settings->get('nullable')); + } +} diff --git a/tests/Domain/Account/AppPassword/AppPasswordTest.php b/tests/Domain/Account/AppPassword/AppPasswordTest.php new file mode 100644 index 0000000..41ad128 --- /dev/null +++ b/tests/Domain/Account/AppPassword/AppPasswordTest.php @@ -0,0 +1,63 @@ +assertSame('did:web:alice.pds.test', $appPassword->getDid()); + $this->assertSame('phone', $appPassword->getName()); + $this->assertSame('hash', $appPassword->getPasswordScrypt()); + $this->assertEquals($createdAt, $appPassword->getCreatedAt()); + $this->assertTrue($appPassword->isPrivileged()); + } + + public function testPrivilegedDefaultsToFalse(): void + { + $appPassword = new AppPassword( + did: 'did:web:alice.pds.test', + name: 'phone', + passwordScrypt: 'hash', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertFalse($appPassword->isPrivileged()); + } + + public function testJsonSerializeOmitsPasswordScryptAndFormatsDate(): void + { + $appPassword = new AppPassword( + did: 'did:web:alice.pds.test', + name: 'phone', + passwordScrypt: 'secret', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + privileged: false, + ); + + $json = json_decode((string) json_encode($appPassword), true); + + $this->assertSame([ + 'did' => 'did:web:alice.pds.test', + 'name' => 'phone', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'privileged' => false, + ], $json); + $this->assertArrayNotHasKey('passwordScrypt', $json); + } +} diff --git a/tests/Domain/Account/EmailToken/EmailTokenTest.php b/tests/Domain/Account/EmailToken/EmailTokenTest.php new file mode 100644 index 0000000..9ee478e --- /dev/null +++ b/tests/Domain/Account/EmailToken/EmailTokenTest.php @@ -0,0 +1,56 @@ +assertSame(EmailToken::PURPOSE_CONFIRM_EMAIL, $token->getPurpose()); + $this->assertSame('did:web:alice.pds.test', $token->getDid()); + $this->assertSame('abc-123', $token->getToken()); + $this->assertEquals($requestedAt, $token->getRequestedAt()); + } + + public function testJsonSerializeOmitsTokenAndFormatsDate(): void + { + $token = new EmailToken( + purpose: EmailToken::PURPOSE_RESET_PASSWORD, + did: 'did:web:alice.pds.test', + token: 'secret-token', + requestedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $json = json_decode((string) json_encode($token), true); + + $this->assertSame([ + 'purpose' => EmailToken::PURPOSE_RESET_PASSWORD, + 'did' => 'did:web:alice.pds.test', + 'requestedAt' => '2026-01-01T00:00:00+00:00', + ], $json); + $this->assertArrayNotHasKey('token', $json); + } + + public function testPurposeConstantsHaveExpectedValues(): void + { + $this->assertSame('confirm_email', EmailToken::PURPOSE_CONFIRM_EMAIL); + $this->assertSame('update_email', EmailToken::PURPOSE_UPDATE_EMAIL); + $this->assertSame('reset_password', EmailToken::PURPOSE_RESET_PASSWORD); + $this->assertSame('delete_account', EmailToken::PURPOSE_DELETE_ACCOUNT); + $this->assertSame('plc_operation', EmailToken::PURPOSE_PLC_OPERATION); + } +} diff --git a/tests/Domain/Account/InviteCode/InviteCodeTest.php b/tests/Domain/Account/InviteCode/InviteCodeTest.php new file mode 100644 index 0000000..6433d25 --- /dev/null +++ b/tests/Domain/Account/InviteCode/InviteCodeTest.php @@ -0,0 +1,55 @@ +assertSame('pds-test-abc123', $code->getCode()); + $this->assertSame(5, $code->getAvailableUses()); + $this->assertFalse($code->isDisabled()); + $this->assertSame('did:web:alice.pds.test', $code->getForAccount()); + $this->assertSame('admin', $code->getCreatedBy()); + $this->assertEquals($createdAt, $code->getCreatedAt()); + } + + public function testJsonSerialize(): void + { + $code = new InviteCode( + code: 'pds-test-abc123', + availableUses: 3, + disabled: true, + forAccount: 'did:web:alice.pds.test', + createdBy: 'admin', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $json = json_decode((string) json_encode($code), true); + + $this->assertSame([ + 'code' => 'pds-test-abc123', + 'availableUses' => 3, + 'disabled' => true, + 'forAccount' => 'did:web:alice.pds.test', + 'createdBy' => 'admin', + 'createdAt' => '2026-01-01T00:00:00+00:00', + ], $json); + } +} diff --git a/tests/Domain/Account/InviteCode/InviteCodeUseTest.php b/tests/Domain/Account/InviteCode/InviteCodeUseTest.php new file mode 100644 index 0000000..2bd8e57 --- /dev/null +++ b/tests/Domain/Account/InviteCode/InviteCodeUseTest.php @@ -0,0 +1,43 @@ +assertSame('pds-test-abc123', $use->getCode()); + $this->assertSame('did:web:alice.pds.test', $use->getUsedBy()); + $this->assertEquals($usedAt, $use->getUsedAt()); + } + + public function testJsonSerialize(): void + { + $use = new InviteCodeUse( + code: 'pds-test-abc123', + usedBy: 'did:web:alice.pds.test', + usedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $json = json_decode((string) json_encode($use), true); + + $this->assertSame([ + 'code' => 'pds-test-abc123', + 'usedBy' => 'did:web:alice.pds.test', + 'usedAt' => '2026-01-01T00:00:00+00:00', + ], $json); + } +} diff --git a/tests/Domain/Account/RefreshToken/RefreshTokenTest.php b/tests/Domain/Account/RefreshToken/RefreshTokenTest.php new file mode 100644 index 0000000..2171f22 --- /dev/null +++ b/tests/Domain/Account/RefreshToken/RefreshTokenTest.php @@ -0,0 +1,61 @@ +assertSame('rt-1', $token->getId()); + $this->assertSame('did:web:alice.pds.test', $token->getDid()); + $this->assertSame('2026-02-01T00:00:00Z', $token->getExpiresAt()); + $this->assertSame('phone', $token->getAppPasswordName()); + $this->assertSame('rt-2', $token->getNextId()); + } + + public function testNullableFields(): void + { + $token = new RefreshToken( + id: 'rt-1', + did: 'did:web:alice.pds.test', + expiresAt: '2026-02-01T00:00:00Z', + appPasswordName: null, + nextId: null, + ); + + $this->assertNull($token->getAppPasswordName()); + $this->assertNull($token->getNextId()); + } + + public function testJsonSerialize(): void + { + $token = new RefreshToken( + id: 'rt-1', + did: 'did:web:alice.pds.test', + expiresAt: '2026-02-01T00:00:00Z', + appPasswordName: 'phone', + nextId: 'rt-2', + ); + + $this->assertSame([ + 'id' => 'rt-1', + 'did' => 'did:web:alice.pds.test', + 'expiresAt' => '2026-02-01T00:00:00Z', + 'appPasswordName' => 'phone', + 'nextId' => 'rt-2', + ], $token->jsonSerialize()); + } +} diff --git a/tests/Domain/Blob/BlobTest.php b/tests/Domain/Blob/BlobTest.php new file mode 100644 index 0000000..b7ad8e9 --- /dev/null +++ b/tests/Domain/Blob/BlobTest.php @@ -0,0 +1,67 @@ +assertSame('bafy-blob', $blob->getCid()); + $this->assertSame('image/png', $blob->getMimeType()); + $this->assertSame(2048, $blob->getSize()); + $this->assertSame('tmp-1', $blob->getTempKey()); + $this->assertEquals($createdAt, $blob->getCreatedAt()); + $this->assertSame('tk-1', $blob->getTakedownRef()); + } + + public function testDefaultTakedownRefIsNullAndTempKeyMayBeNull(): void + { + $blob = new Blob( + cid: 'bafy-blob', + mimeType: 'image/png', + size: 2048, + tempKey: null, + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertNull($blob->getTempKey()); + $this->assertNull($blob->getTakedownRef()); + } + + public function testJsonSerialize(): void + { + $blob = new Blob( + cid: 'bafy-blob', + mimeType: 'image/png', + size: 2048, + tempKey: 'tmp-1', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + takedownRef: null, + ); + + $this->assertSame([ + 'cid' => 'bafy-blob', + 'mimeType' => 'image/png', + 'size' => 2048, + 'tempKey' => 'tmp-1', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'takedownRef' => null, + ], $blob->jsonSerialize()); + } +} diff --git a/tests/Domain/Did/DidDocEntryTest.php b/tests/Domain/Did/DidDocEntryTest.php new file mode 100644 index 0000000..965b7b8 --- /dev/null +++ b/tests/Domain/Did/DidDocEntryTest.php @@ -0,0 +1,43 @@ + 'did:plc:alice', 'verificationMethod' => []]; + + $entry = new DidDocEntry( + did: 'did:plc:alice', + doc: $doc, + updatedAt: $updatedAt, + ); + + $this->assertSame('did:plc:alice', $entry->getDid()); + $this->assertSame($doc, $entry->getDoc()); + $this->assertEquals($updatedAt, $entry->getUpdatedAt()); + } + + public function testJsonSerialize(): void + { + $entry = new DidDocEntry( + did: 'did:plc:alice', + doc: ['id' => 'did:plc:alice'], + updatedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertSame([ + 'did' => 'did:plc:alice', + 'doc' => ['id' => 'did:plc:alice'], + 'updatedAt' => '2026-01-01T00:00:00+00:00', + ], $entry->jsonSerialize()); + } +} diff --git a/tests/Domain/Lexicon/LexiconEntryTest.php b/tests/Domain/Lexicon/LexiconEntryTest.php new file mode 100644 index 0000000..ba864e5 --- /dev/null +++ b/tests/Domain/Lexicon/LexiconEntryTest.php @@ -0,0 +1,92 @@ + 1, 'defs' => []]; + + $entry = new LexiconEntry( + nsid: 'com.example.foo', + createdAt: $createdAt, + updatedAt: $updatedAt, + lastSucceededAt: $lastSucceededAt, + uri: 'https://example.com/foo.json', + lexicon: $lex, + ); + + $this->assertSame('com.example.foo', $entry->getNsid()); + $this->assertEquals($createdAt, $entry->getCreatedAt()); + $this->assertEquals($updatedAt, $entry->getUpdatedAt()); + $this->assertEquals($lastSucceededAt, $entry->getLastSucceededAt()); + $this->assertSame('https://example.com/foo.json', $entry->getUri()); + $this->assertSame($lex, $entry->getLexicon()); + } + + public function testNullableFields(): void + { + $entry = new LexiconEntry( + nsid: 'com.example.foo', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + lastSucceededAt: null, + uri: null, + lexicon: null, + ); + + $this->assertNull($entry->getLastSucceededAt()); + $this->assertNull($entry->getUri()); + $this->assertNull($entry->getLexicon()); + } + + public function testJsonSerializeOmitsLexicon(): void + { + $entry = new LexiconEntry( + nsid: 'com.example.foo', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + lastSucceededAt: new DateTimeImmutable('2026-01-02T01:00:00Z'), + uri: 'https://example.com/foo.json', + lexicon: ['lexicon' => 1], + ); + + $json = json_decode((string) json_encode($entry), true); + + $this->assertSame([ + 'nsid' => 'com.example.foo', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'updatedAt' => '2026-01-02T00:00:00+00:00', + 'lastSucceededAt' => '2026-01-02T01:00:00+00:00', + 'uri' => 'https://example.com/foo.json', + ], $json); + $this->assertArrayNotHasKey('lexicon', $json); + } + + public function testJsonSerializeWithNullLastSucceededAt(): void + { + $entry = new LexiconEntry( + nsid: 'com.example.foo', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + lastSucceededAt: null, + uri: null, + lexicon: null, + ); + + $payload = $entry->jsonSerialize(); + + $this->assertNull($payload['lastSucceededAt']); + $this->assertNull($payload['uri']); + } +} diff --git a/tests/Domain/OAuth/AccountDeviceTest.php b/tests/Domain/OAuth/AccountDeviceTest.php new file mode 100644 index 0000000..20f740d --- /dev/null +++ b/tests/Domain/OAuth/AccountDeviceTest.php @@ -0,0 +1,46 @@ +assertSame('did:plc:alice', $device->getDid()); + $this->assertSame('dev-1', $device->getDeviceId()); + $this->assertEquals($createdAt, $device->getCreatedAt()); + $this->assertEquals($updatedAt, $device->getUpdatedAt()); + } + + public function testJsonSerialize(): void + { + $device = new AccountDevice( + did: 'did:plc:alice', + deviceId: 'dev-1', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + ); + + $this->assertSame([ + 'did' => 'did:plc:alice', + 'deviceId' => 'dev-1', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'updatedAt' => '2026-01-02T00:00:00+00:00', + ], $device->jsonSerialize()); + } +} diff --git a/tests/Domain/OAuth/AuthorizationRequestTest.php b/tests/Domain/OAuth/AuthorizationRequestTest.php new file mode 100644 index 0000000..f2bd2fc --- /dev/null +++ b/tests/Domain/OAuth/AuthorizationRequestTest.php @@ -0,0 +1,86 @@ + 'none']; + $params = ['scope' => 'atproto']; + + $req = new AuthorizationRequest( + id: 'req-1', + did: 'did:plc:alice', + deviceId: 'dev-1', + clientId: 'https://app.test/client.json', + clientAuth: $clientAuth, + parameters: $params, + expiresAt: $expiresAt, + code: 'auth-code-1', + ); + + $this->assertSame('req-1', $req->getId()); + $this->assertSame('did:plc:alice', $req->getDid()); + $this->assertSame('dev-1', $req->getDeviceId()); + $this->assertSame('https://app.test/client.json', $req->getClientId()); + $this->assertSame($clientAuth, $req->getClientAuth()); + $this->assertSame($params, $req->getParameters()); + $this->assertEquals($expiresAt, $req->getExpiresAt()); + $this->assertSame('auth-code-1', $req->getCode()); + } + + public function testNullableFields(): void + { + $req = new AuthorizationRequest( + id: 'req-1', + did: null, + deviceId: null, + clientId: 'https://app.test/client.json', + clientAuth: null, + parameters: [], + expiresAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + code: null, + ); + + $this->assertNull($req->getDid()); + $this->assertNull($req->getDeviceId()); + $this->assertNull($req->getClientAuth()); + $this->assertSame([], $req->getParameters()); + $this->assertNull($req->getCode()); + } + + public function testJsonSerializeOmitsClientAuthAndParameters(): void + { + $req = new AuthorizationRequest( + id: 'req-1', + did: 'did:plc:alice', + deviceId: 'dev-1', + clientId: 'https://app.test/client.json', + clientAuth: ['method' => 'secret'], + parameters: ['scope' => 'atproto'], + expiresAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + code: 'auth-code-1', + ); + + $json = json_decode((string) json_encode($req), true); + + $this->assertSame([ + 'id' => 'req-1', + 'did' => 'did:plc:alice', + 'deviceId' => 'dev-1', + 'clientId' => 'https://app.test/client.json', + 'expiresAt' => '2026-01-01T00:00:00+00:00', + 'code' => 'auth-code-1', + ], $json); + $this->assertArrayNotHasKey('clientAuth', $json); + $this->assertArrayNotHasKey('parameters', $json); + } +} diff --git a/tests/Domain/OAuth/AuthorizedClientTest.php b/tests/Domain/OAuth/AuthorizedClientTest.php new file mode 100644 index 0000000..90e95a3 --- /dev/null +++ b/tests/Domain/OAuth/AuthorizedClientTest.php @@ -0,0 +1,54 @@ + ['atproto']]; + + $client = new AuthorizedClient( + did: 'did:plc:alice', + clientId: 'https://app.test/client.json', + createdAt: $createdAt, + updatedAt: $updatedAt, + data: $data, + ); + + $this->assertSame('did:plc:alice', $client->getDid()); + $this->assertSame('https://app.test/client.json', $client->getClientId()); + $this->assertEquals($createdAt, $client->getCreatedAt()); + $this->assertEquals($updatedAt, $client->getUpdatedAt()); + $this->assertSame($data, $client->getData()); + } + + public function testJsonSerializeOmitsData(): void + { + $client = new AuthorizedClient( + did: 'did:plc:alice', + clientId: 'https://app.test/client.json', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + data: ['secret' => 'shh'], + ); + + $json = json_decode((string) json_encode($client), true); + + $this->assertSame([ + 'did' => 'did:plc:alice', + 'clientId' => 'https://app.test/client.json', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'updatedAt' => '2026-01-02T00:00:00+00:00', + ], $json); + $this->assertArrayNotHasKey('data', $json); + } +} diff --git a/tests/Domain/OAuth/DeviceTest.php b/tests/Domain/OAuth/DeviceTest.php new file mode 100644 index 0000000..224dab1 --- /dev/null +++ b/tests/Domain/OAuth/DeviceTest.php @@ -0,0 +1,62 @@ +assertSame('dev-1', $device->getId()); + $this->assertSame('sess-1', $device->getSessionId()); + $this->assertSame('Mozilla/5.0', $device->getUserAgent()); + $this->assertSame('127.0.0.1', $device->getIpAddress()); + $this->assertEquals($lastSeenAt, $device->getLastSeenAt()); + } + + public function testUserAgentCanBeNull(): void + { + $device = new Device( + id: 'dev-1', + sessionId: 'sess-1', + userAgent: null, + ipAddress: '127.0.0.1', + lastSeenAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertNull($device->getUserAgent()); + } + + public function testJsonSerialize(): void + { + $device = new Device( + id: 'dev-1', + sessionId: 'sess-1', + userAgent: 'Mozilla/5.0', + ipAddress: '127.0.0.1', + lastSeenAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertSame([ + 'id' => 'dev-1', + 'sessionId' => 'sess-1', + 'userAgent' => 'Mozilla/5.0', + 'ipAddress' => '127.0.0.1', + 'lastSeenAt' => '2026-01-01T00:00:00+00:00', + ], $device->jsonSerialize()); + } +} diff --git a/tests/Domain/OAuth/OAuthTokenTest.php b/tests/Domain/OAuth/OAuthTokenTest.php new file mode 100644 index 0000000..c79b479 --- /dev/null +++ b/tests/Domain/OAuth/OAuthTokenTest.php @@ -0,0 +1,101 @@ + 'none'], + deviceId: 'dev-1', + parameters: ['scope' => 'atproto'], + details: ['extra' => true], + code: 'auth-code', + currentRefreshToken: 'refresh-1', + scope: 'atproto', + ); + } + + public function testGetters(): void + { + $token = $this->makeToken(); + + $this->assertSame(42, $token->getId()); + $this->assertSame('did:plc:alice', $token->getDid()); + $this->assertSame('tok-1', $token->getTokenId()); + $this->assertEquals(new DateTimeImmutable('2026-01-01T00:00:00Z'), $token->getCreatedAt()); + $this->assertEquals(new DateTimeImmutable('2026-01-02T00:00:00Z'), $token->getUpdatedAt()); + $this->assertEquals(new DateTimeImmutable('2026-02-01T00:00:00Z'), $token->getExpiresAt()); + $this->assertSame('https://app.test/client.json', $token->getClientId()); + $this->assertSame(['method' => 'none'], $token->getClientAuth()); + $this->assertSame('dev-1', $token->getDeviceId()); + $this->assertSame(['scope' => 'atproto'], $token->getParameters()); + $this->assertSame(['extra' => true], $token->getDetails()); + $this->assertSame('auth-code', $token->getCode()); + $this->assertSame('refresh-1', $token->getCurrentRefreshToken()); + $this->assertSame('atproto', $token->getScope()); + } + + public function testNullableFields(): void + { + $token = new OAuthToken( + id: 1, + did: 'did:plc:alice', + tokenId: 'tok-1', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-01-02T00:00:00Z'), + expiresAt: new DateTimeImmutable('2026-02-01T00:00:00Z'), + clientId: 'https://app.test/client.json', + clientAuth: [], + deviceId: null, + parameters: [], + details: null, + code: null, + currentRefreshToken: null, + scope: null, + ); + + $this->assertNull($token->getDeviceId()); + $this->assertNull($token->getDetails()); + $this->assertNull($token->getCode()); + $this->assertNull($token->getCurrentRefreshToken()); + $this->assertNull($token->getScope()); + } + + public function testJsonSerializeOmitsSensitiveJsonColumns(): void + { + $token = $this->makeToken(); + $json = json_decode((string) json_encode($token), true); + + $this->assertSame([ + 'id' => 42, + 'did' => 'did:plc:alice', + 'tokenId' => 'tok-1', + 'createdAt' => '2026-01-01T00:00:00+00:00', + 'updatedAt' => '2026-01-02T00:00:00+00:00', + 'expiresAt' => '2026-02-01T00:00:00+00:00', + 'clientId' => 'https://app.test/client.json', + 'deviceId' => 'dev-1', + 'code' => 'auth-code', + 'currentRefreshToken' => 'refresh-1', + 'scope' => 'atproto', + ], $json); + $this->assertArrayNotHasKey('clientAuth', $json); + $this->assertArrayNotHasKey('parameters', $json); + $this->assertArrayNotHasKey('details', $json); + } +} diff --git a/tests/Domain/OAuth/UsedRefreshTokenTest.php b/tests/Domain/OAuth/UsedRefreshTokenTest.php new file mode 100644 index 0000000..edf73e1 --- /dev/null +++ b/tests/Domain/OAuth/UsedRefreshTokenTest.php @@ -0,0 +1,29 @@ +assertSame(7, $used->getTokenId()); + $this->assertSame('refresh-xyz', $used->getRefreshToken()); + } + + public function testJsonSerialize(): void + { + $used = new UsedRefreshToken(tokenId: 7, refreshToken: 'refresh-xyz'); + + $this->assertSame( + ['tokenId' => 7, 'refreshToken' => 'refresh-xyz'], + $used->jsonSerialize() + ); + } +} diff --git a/tests/Domain/Pds/Atproto/Identity/ResolveHandleResponseTest.php b/tests/Domain/Pds/Atproto/Identity/ResolveHandleResponseTest.php new file mode 100644 index 0000000..2d5a588 --- /dev/null +++ b/tests/Domain/Pds/Atproto/Identity/ResolveHandleResponseTest.php @@ -0,0 +1,26 @@ +assertSame('did:plc:alice', $response->getDid()); + } + + public function testJsonSerialize(): void + { + $response = new ResolveHandleResponse('did:plc:alice'); + + $this->assertSame(['did' => 'did:plc:alice'], $response->jsonSerialize()); + $this->assertSame('{"did":"did:plc:alice"}', (string) json_encode($response)); + } +} diff --git a/tests/Domain/Preference/AccountPrefTest.php b/tests/Domain/Preference/AccountPrefTest.php new file mode 100644 index 0000000..49fb6c1 --- /dev/null +++ b/tests/Domain/Preference/AccountPrefTest.php @@ -0,0 +1,39 @@ +assertSame(1, $pref->getId()); + $this->assertSame('app.bsky.actor.defs#savedFeedsPref', $pref->getName()); + $this->assertSame('{"items":[]}', $pref->getValueJson()); + } + + public function testJsonSerialize(): void + { + $pref = new AccountPref( + id: 1, + name: 'app.bsky.actor.defs#savedFeedsPref', + valueJson: '{"items":[]}', + ); + + $this->assertSame([ + 'id' => 1, + 'name' => 'app.bsky.actor.defs#savedFeedsPref', + 'valueJson' => '{"items":[]}', + ], $pref->jsonSerialize()); + } +} diff --git a/tests/Domain/Record/BacklinkTest.php b/tests/Domain/Record/BacklinkTest.php new file mode 100644 index 0000000..b48d02b --- /dev/null +++ b/tests/Domain/Record/BacklinkTest.php @@ -0,0 +1,39 @@ +assertSame('at://did:plc:alice/app.bsky.feed.like/abc', $backlink->getUri()); + $this->assertSame('subject.uri', $backlink->getPath()); + $this->assertSame('at://did:plc:bob/app.bsky.feed.post/xyz', $backlink->getLinkTo()); + } + + public function testJsonSerialize(): void + { + $backlink = new Backlink( + uri: 'at://did:plc:alice/app.bsky.feed.like/abc', + path: 'subject.uri', + linkTo: 'at://did:plc:bob/app.bsky.feed.post/xyz', + ); + + $this->assertSame([ + 'uri' => 'at://did:plc:alice/app.bsky.feed.like/abc', + 'path' => 'subject.uri', + 'linkTo' => 'at://did:plc:bob/app.bsky.feed.post/xyz', + ], $backlink->jsonSerialize()); + } +} diff --git a/tests/Domain/Record/RecordBlobTest.php b/tests/Domain/Record/RecordBlobTest.php new file mode 100644 index 0000000..048e091 --- /dev/null +++ b/tests/Domain/Record/RecordBlobTest.php @@ -0,0 +1,35 @@ +assertSame('bafy-blob', $rb->getBlobCid()); + $this->assertSame('at://did:plc:alice/app.bsky.feed.post/3k', $rb->getRecordUri()); + } + + public function testJsonSerialize(): void + { + $rb = new RecordBlob( + blobCid: 'bafy-blob', + recordUri: 'at://did:plc:alice/app.bsky.feed.post/3k', + ); + + $this->assertSame([ + 'blobCid' => 'bafy-blob', + 'recordUri' => 'at://did:plc:alice/app.bsky.feed.post/3k', + ], $rb->jsonSerialize()); + } +} diff --git a/tests/Domain/Record/RecordTest.php b/tests/Domain/Record/RecordTest.php new file mode 100644 index 0000000..3c2a431 --- /dev/null +++ b/tests/Domain/Record/RecordTest.php @@ -0,0 +1,71 @@ +assertSame('at://did:plc:alice/app.bsky.feed.post/3k', $record->getUri()); + $this->assertSame('bafy-record', $record->getCid()); + $this->assertSame('app.bsky.feed.post', $record->getCollection()); + $this->assertSame('3k', $record->getRkey()); + $this->assertSame('rev-1', $record->getRepoRev()); + $this->assertEquals($indexedAt, $record->getIndexedAt()); + $this->assertSame('tk-1', $record->getTakedownRef()); + } + + public function testTakedownRefDefaultsToNull(): void + { + $record = new Record( + uri: 'at://did:plc:alice/app.bsky.feed.post/3k', + cid: 'bafy-record', + collection: 'app.bsky.feed.post', + rkey: '3k', + repoRev: 'rev-1', + indexedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertNull($record->getTakedownRef()); + } + + public function testJsonSerialize(): void + { + $record = new Record( + uri: 'at://did:plc:alice/app.bsky.feed.post/3k', + cid: 'bafy-record', + collection: 'app.bsky.feed.post', + rkey: '3k', + repoRev: 'rev-1', + indexedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + takedownRef: null, + ); + + $this->assertSame([ + 'uri' => 'at://did:plc:alice/app.bsky.feed.post/3k', + 'cid' => 'bafy-record', + 'collection' => 'app.bsky.feed.post', + 'rkey' => '3k', + 'repoRev' => 'rev-1', + 'indexedAt' => '2026-01-01T00:00:00+00:00', + 'takedownRef' => null, + ], $record->jsonSerialize()); + } +} diff --git a/tests/Domain/Repo/CborBytesTest.php b/tests/Domain/Repo/CborBytesTest.php new file mode 100644 index 0000000..833bbd3 --- /dev/null +++ b/tests/Domain/Repo/CborBytesTest.php @@ -0,0 +1,26 @@ +assertSame($raw, $wrapped->getBytes()); + } + + public function testSupportsEmptyByteString(): void + { + $wrapped = new CborBytes(''); + + $this->assertSame('', $wrapped->getBytes()); + } +} diff --git a/tests/Domain/Repo/CidLinkTest.php b/tests/Domain/Repo/CidLinkTest.php new file mode 100644 index 0000000..7ae5825 --- /dev/null +++ b/tests/Domain/Repo/CidLinkTest.php @@ -0,0 +1,21 @@ +assertSame( + 'bafyreigh2akiscaildc3o3vhbw5dvz25ftpqhmkfymf2e3glr3nzxqvxha', + $link->getCid() + ); + } +} diff --git a/tests/Domain/Repo/RepoBlockTest.php b/tests/Domain/Repo/RepoBlockTest.php new file mode 100644 index 0000000..ca4a694 --- /dev/null +++ b/tests/Domain/Repo/RepoBlockTest.php @@ -0,0 +1,45 @@ +assertSame('bafy-cid', $block->getCid()); + $this->assertSame('rev-1', $block->getRepoRev()); + $this->assertSame(128, $block->getSize()); + $this->assertSame("\x00binary\xffbytes", $block->getContent()); + } + + public function testJsonSerializeOmitsContent(): void + { + $block = new RepoBlock( + cid: 'bafy-cid', + repoRev: 'rev-1', + size: 128, + content: "\x00binary\xffbytes", + ); + + $payload = $block->jsonSerialize(); + + $this->assertSame([ + 'cid' => 'bafy-cid', + 'repoRev' => 'rev-1', + 'size' => 128, + ], $payload); + $this->assertArrayNotHasKey('content', $payload); + } +} diff --git a/tests/Domain/Repo/RepoRootTest.php b/tests/Domain/Repo/RepoRootTest.php new file mode 100644 index 0000000..2a0bebf --- /dev/null +++ b/tests/Domain/Repo/RepoRootTest.php @@ -0,0 +1,45 @@ +assertSame('did:plc:alice', $root->getDid()); + $this->assertSame('bafy-root', $root->getCid()); + $this->assertSame('rev-1', $root->getRev()); + $this->assertEquals($indexedAt, $root->getIndexedAt()); + } + + public function testJsonSerialize(): void + { + $root = new RepoRoot( + did: 'did:plc:alice', + cid: 'bafy-root', + rev: 'rev-1', + indexedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertSame([ + 'did' => 'did:plc:alice', + 'cid' => 'bafy-root', + 'rev' => 'rev-1', + 'indexedAt' => '2026-01-01T00:00:00+00:00', + ], $root->jsonSerialize()); + } +} diff --git a/tests/Domain/Sequencer/RepoSeqEventTest.php b/tests/Domain/Sequencer/RepoSeqEventTest.php new file mode 100644 index 0000000..b374c0f --- /dev/null +++ b/tests/Domain/Sequencer/RepoSeqEventTest.php @@ -0,0 +1,77 @@ +assertSame(42, $event->getSeq()); + $this->assertSame('did:plc:alice', $event->getDid()); + $this->assertSame(RepoSeqEvent::TYPE_APPEND, $event->getEventType()); + $this->assertSame("\x00cbor", $event->getEvent()); + $this->assertEquals($sequencedAt, $event->getSequencedAt()); + $this->assertTrue($event->isInvalidated()); + } + + public function testInvalidatedDefaultsToFalse(): void + { + $event = new RepoSeqEvent( + seq: 1, + did: 'did:plc:alice', + eventType: RepoSeqEvent::TYPE_SYNC, + event: '', + sequencedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + ); + + $this->assertFalse($event->isInvalidated()); + } + + public function testJsonSerializeOmitsEventBytes(): void + { + $event = new RepoSeqEvent( + seq: 42, + did: 'did:plc:alice', + eventType: RepoSeqEvent::TYPE_APPEND, + event: "\x00binary", + sequencedAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + invalidated: false, + ); + + $json = json_decode((string) json_encode($event), true); + + $this->assertSame([ + 'seq' => 42, + 'did' => 'did:plc:alice', + 'eventType' => RepoSeqEvent::TYPE_APPEND, + 'sequencedAt' => '2026-01-01T00:00:00+00:00', + 'invalidated' => false, + ], $json); + $this->assertArrayNotHasKey('event', $json); + } + + public function testTypeConstants(): void + { + $this->assertSame('append', RepoSeqEvent::TYPE_APPEND); + $this->assertSame('sync', RepoSeqEvent::TYPE_SYNC); + $this->assertSame('identity', RepoSeqEvent::TYPE_IDENTITY); + $this->assertSame('account', RepoSeqEvent::TYPE_ACCOUNT); + $this->assertSame('tombstone', RepoSeqEvent::TYPE_TOMBSTONE); + } +} diff --git a/tests/Infrastructure/Persistence/Lexicon/InMemoryLexiconRepositoryTest.php b/tests/Infrastructure/Persistence/Lexicon/InMemoryLexiconRepositoryTest.php new file mode 100644 index 0000000..b466775 --- /dev/null +++ b/tests/Infrastructure/Persistence/Lexicon/InMemoryLexiconRepositoryTest.php @@ -0,0 +1,80 @@ +makeEntry(); + $repo = new InMemoryLexiconRepository([$entry]); + + $this->assertSame($entry, $repo->findByNsid('com.example.foo')); + } + + public function testFindByNsidThrowsWhenMissing(): void + { + $repo = new InMemoryLexiconRepository(); + + $this->expectException(LexiconEntryNotFoundException::class); + $repo->findByNsid('nope'); + } + + public function testFindAll(): void + { + $repo = new InMemoryLexiconRepository([ + $this->makeEntry('com.example.a'), + $this->makeEntry('com.example.b'), + ]); + + $this->assertCount(2, $repo->findAll()); + } + + public function testSaveInsertsAndUpdates(): void + { + $repo = new InMemoryLexiconRepository(); + $repo->save($this->makeEntry('com.example.a')); + $this->assertCount(1, $repo->findAll()); + + $updated = new LexiconEntry( + nsid: 'com.example.a', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-02-01T00:00:00Z'), + lastSucceededAt: null, + uri: 'https://example.com/a.json', + lexicon: null, + ); + $repo->save($updated); + + $this->assertCount(1, $repo->findAll()); + $this->assertSame('https://example.com/a.json', $repo->findByNsid('com.example.a')->getUri()); + } + + public function testDeleteByNsid(): void + { + $repo = new InMemoryLexiconRepository([$this->makeEntry()]); + $repo->deleteByNsid('com.example.foo'); + + $this->assertCount(0, $repo->findAll()); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryAccountDeviceRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryAccountDeviceRepositoryTest.php new file mode 100644 index 0000000..389dcd2 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryAccountDeviceRepositoryTest.php @@ -0,0 +1,88 @@ +makeEntry(); + $repo = new InMemoryAccountDeviceRepository([$entry]); + + $this->assertSame($entry, $repo->findByDidAndDeviceId('did:plc:alice', 'dev-1')); + } + + public function testFindByDidAndDeviceIdThrowsWhenMissing(): void + { + $repo = new InMemoryAccountDeviceRepository(); + + $this->expectException(AccountDeviceNotFoundException::class); + $repo->findByDidAndDeviceId('did:plc:alice', 'dev-1'); + } + + public function testFindAllForDid(): void + { + $repo = new InMemoryAccountDeviceRepository([ + $this->makeEntry('did:plc:alice', 'dev-1'), + $this->makeEntry('did:plc:alice', 'dev-2'), + $this->makeEntry('did:plc:bob', 'dev-3'), + ]); + + $this->assertCount(2, $repo->findAllForDid('did:plc:alice')); + $this->assertCount(1, $repo->findAllForDid('did:plc:bob')); + $this->assertCount(0, $repo->findAllForDid('did:plc:carol')); + } + + public function testSaveInsertsNewAndUpdatesExisting(): void + { + $repo = new InMemoryAccountDeviceRepository(); + $repo->save($this->makeEntry()); + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + + $updated = new AccountDevice( + did: 'did:plc:alice', + deviceId: 'dev-1', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-02-01T00:00:00Z'), + ); + $repo->save($updated); + + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + $this->assertEquals( + new DateTimeImmutable('2026-02-01T00:00:00Z'), + $repo->findByDidAndDeviceId('did:plc:alice', 'dev-1')->getUpdatedAt(), + ); + } + + public function testDeleteByDidAndDeviceId(): void + { + $repo = new InMemoryAccountDeviceRepository([ + $this->makeEntry('did:plc:alice', 'dev-1'), + $this->makeEntry('did:plc:alice', 'dev-2'), + ]); + + $repo->deleteByDidAndDeviceId('did:plc:alice', 'dev-1'); + + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + $this->expectException(AccountDeviceNotFoundException::class); + $repo->findByDidAndDeviceId('did:plc:alice', 'dev-1'); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizationRequestRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizationRequestRepositoryTest.php new file mode 100644 index 0000000..97d99e1 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizationRequestRepositoryTest.php @@ -0,0 +1,88 @@ +makeRequest(); + $repo = new InMemoryAuthorizationRequestRepository([$req]); + + $this->assertSame($req, $repo->findById('req-1')); + $this->assertSame($req, $repo->findByCode('auth-code')); + } + + public function testFindByIdThrowsWhenMissing(): void + { + $repo = new InMemoryAuthorizationRequestRepository(); + + $this->expectException(AuthorizationRequestNotFoundException::class); + $repo->findById('nope'); + } + + public function testFindByCodeThrowsWhenMissing(): void + { + $repo = new InMemoryAuthorizationRequestRepository([$this->makeRequest(code: null)]); + + $this->expectException(AuthorizationRequestNotFoundException::class); + $repo->findByCode('nope'); + } + + public function testSaveInsertsAndUpdates(): void + { + $repo = new InMemoryAuthorizationRequestRepository(); + $repo->save($this->makeRequest(id: 'req-1', code: 'a')); + $repo->save($this->makeRequest(id: 'req-1', code: 'b')); + + $this->assertSame('b', $repo->findById('req-1')->getCode()); + } + + public function testDeleteById(): void + { + $repo = new InMemoryAuthorizationRequestRepository([$this->makeRequest()]); + $repo->deleteById('req-1'); + + $this->expectException(AuthorizationRequestNotFoundException::class); + $repo->findById('req-1'); + } + + public function testDeleteExpiredRemovesPastEntriesOnly(): void + { + $past = $this->makeRequest(id: 'expired', expiresAt: new DateTimeImmutable('-1 hour')); + $future = $this->makeRequest(id: 'live', expiresAt: new DateTimeImmutable('+1 hour')); + $repo = new InMemoryAuthorizationRequestRepository([$past, $future]); + + $removed = $repo->deleteExpired(); + + $this->assertSame(1, $removed); + $this->assertSame($future, $repo->findById('live')); + $this->expectException(AuthorizationRequestNotFoundException::class); + $repo->findById('expired'); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizedClientRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizedClientRepositoryTest.php new file mode 100644 index 0000000..2059f53 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryAuthorizedClientRepositoryTest.php @@ -0,0 +1,103 @@ +makeEntry(); + $repo = new InMemoryAuthorizedClientRepository([$entry]); + + $this->assertSame($entry, $repo->findByDidAndClientId('did:plc:alice', 'https://app.test/client.json')); + } + + public function testFindByDidAndClientIdThrowsWhenMissing(): void + { + $repo = new InMemoryAuthorizedClientRepository(); + + $this->expectException(AuthorizedClientNotFoundException::class); + $repo->findByDidAndClientId('did:plc:alice', 'nope'); + } + + public function testFindAllForDid(): void + { + $repo = new InMemoryAuthorizedClientRepository([ + $this->makeEntry('did:plc:alice', 'c1'), + $this->makeEntry('did:plc:alice', 'c2'), + $this->makeEntry('did:plc:bob', 'c3'), + ]); + + $this->assertCount(2, $repo->findAllForDid('did:plc:alice')); + $this->assertCount(1, $repo->findAllForDid('did:plc:bob')); + } + + public function testSaveInsertsAndUpdates(): void + { + $repo = new InMemoryAuthorizedClientRepository(); + $repo->save($this->makeEntry()); + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + + $updated = new AuthorizedClient( + did: 'did:plc:alice', + clientId: 'https://app.test/client.json', + createdAt: new DateTimeImmutable('2026-01-01T00:00:00Z'), + updatedAt: new DateTimeImmutable('2026-02-01T00:00:00Z'), + data: ['new' => true], + ); + $repo->save($updated); + + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + $this->assertSame( + ['new' => true], + $repo->findByDidAndClientId('did:plc:alice', 'https://app.test/client.json')->getData() + ); + } + + public function testDeleteByDidAndClientId(): void + { + $repo = new InMemoryAuthorizedClientRepository([ + $this->makeEntry('did:plc:alice', 'c1'), + $this->makeEntry('did:plc:alice', 'c2'), + ]); + + $repo->deleteByDidAndClientId('did:plc:alice', 'c1'); + + $this->assertCount(1, $repo->findAllForDid('did:plc:alice')); + } + + public function testDeleteAllForDid(): void + { + $repo = new InMemoryAuthorizedClientRepository([ + $this->makeEntry('did:plc:alice', 'c1'), + $this->makeEntry('did:plc:alice', 'c2'), + $this->makeEntry('did:plc:bob', 'c3'), + ]); + + $repo->deleteAllForDid('did:plc:alice'); + + $this->assertCount(0, $repo->findAllForDid('did:plc:alice')); + $this->assertCount(1, $repo->findAllForDid('did:plc:bob')); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryDeviceRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryDeviceRepositoryTest.php new file mode 100644 index 0000000..d658118 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryDeviceRepositoryTest.php @@ -0,0 +1,67 @@ +makeDevice(); + $repo = new InMemoryDeviceRepository([$device]); + + $this->assertSame($device, $repo->findById('dev-1')); + } + + public function testFindByIdThrowsWhenMissing(): void + { + $repo = new InMemoryDeviceRepository(); + + $this->expectException(DeviceNotFoundException::class); + $repo->findById('nope'); + } + + public function testSaveInsertsAndUpdates(): void + { + $repo = new InMemoryDeviceRepository(); + $repo->save($this->makeDevice()); + + $updated = new Device( + id: 'dev-1', + sessionId: 'sess-updated', + userAgent: 'Mozilla/5.0', + ipAddress: '10.0.0.1', + lastSeenAt: new DateTimeImmutable('2026-02-01T00:00:00Z'), + ); + $repo->save($updated); + + $this->assertSame('sess-updated', $repo->findById('dev-1')->getSessionId()); + } + + public function testDeleteById(): void + { + $repo = new InMemoryDeviceRepository([$this->makeDevice()]); + $repo->deleteById('dev-1'); + + $this->expectException(DeviceNotFoundException::class); + $repo->findById('dev-1'); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryOAuthTokenRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryOAuthTokenRepositoryTest.php new file mode 100644 index 0000000..5a62467 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryOAuthTokenRepositoryTest.php @@ -0,0 +1,128 @@ +makeToken(); + $repo = new InMemoryOAuthTokenRepository([$token]); + + $this->assertSame($token, $repo->findByTokenId('tok-1')); + } + + public function testFindByTokenIdThrowsWhenMissing(): void + { + $repo = new InMemoryOAuthTokenRepository(); + + $this->expectException(OAuthTokenNotFoundException::class); + $repo->findByTokenId('nope'); + } + + public function testFindByCode(): void + { + $repo = new InMemoryOAuthTokenRepository([$this->makeToken()]); + + $this->assertSame('tok-1', $repo->findByCode('code-1')->getTokenId()); + } + + public function testFindByCodeThrowsWhenMissing(): void + { + $repo = new InMemoryOAuthTokenRepository([$this->makeToken(code: null)]); + + $this->expectException(OAuthTokenNotFoundException::class); + $repo->findByCode('nope'); + } + + public function testFindByRefreshToken(): void + { + $repo = new InMemoryOAuthTokenRepository([$this->makeToken()]); + + $this->assertSame('tok-1', $repo->findByRefreshToken('refresh-1')->getTokenId()); + } + + public function testFindByRefreshTokenThrowsWhenMissing(): void + { + $repo = new InMemoryOAuthTokenRepository([$this->makeToken(refresh: null)]); + + $this->expectException(OAuthTokenNotFoundException::class); + $repo->findByRefreshToken('nope'); + } + + public function testFindAllForDid(): void + { + $repo = new InMemoryOAuthTokenRepository([ + $this->makeToken(id: 1, tokenId: 't1', did: 'did:plc:alice', code: 'c1', refresh: 'r1'), + $this->makeToken(id: 2, tokenId: 't2', did: 'did:plc:alice', code: 'c2', refresh: 'r2'), + $this->makeToken(id: 3, tokenId: 't3', did: 'did:plc:bob', code: 'c3', refresh: 'r3'), + ]); + + $this->assertCount(2, $repo->findAllForDid('did:plc:alice')); + $this->assertCount(1, $repo->findAllForDid('did:plc:bob')); + } + + public function testSaveInsertsAndUpdates(): void + { + $repo = new InMemoryOAuthTokenRepository(); + $repo->save($this->makeToken()); + $repo->save($this->makeToken(code: 'updated')); + + $this->assertSame('updated', $repo->findByTokenId('tok-1')->getCode()); + } + + public function testDeleteByTokenId(): void + { + $repo = new InMemoryOAuthTokenRepository([$this->makeToken()]); + $repo->deleteByTokenId('tok-1'); + + $this->expectException(OAuthTokenNotFoundException::class); + $repo->findByTokenId('tok-1'); + } + + public function testDeleteAllForDid(): void + { + $repo = new InMemoryOAuthTokenRepository([ + $this->makeToken(id: 1, tokenId: 't1', did: 'did:plc:alice', code: 'c1', refresh: 'r1'), + $this->makeToken(id: 2, tokenId: 't2', did: 'did:plc:bob', code: 'c2', refresh: 'r2'), + ]); + + $repo->deleteAllForDid('did:plc:alice'); + + $this->assertCount(0, $repo->findAllForDid('did:plc:alice')); + $this->assertCount(1, $repo->findAllForDid('did:plc:bob')); + } +} diff --git a/tests/Infrastructure/Persistence/OAuth/InMemoryUsedRefreshTokenRepositoryTest.php b/tests/Infrastructure/Persistence/OAuth/InMemoryUsedRefreshTokenRepositoryTest.php new file mode 100644 index 0000000..c1d2449 --- /dev/null +++ b/tests/Infrastructure/Persistence/OAuth/InMemoryUsedRefreshTokenRepositoryTest.php @@ -0,0 +1,58 @@ +assertTrue($repo->exists('refresh-a')); + $this->assertFalse($repo->exists('refresh-b')); + } + + public function testSaveMakesTokenExist(): void + { + $repo = new InMemoryUsedRefreshTokenRepository(); + $repo->save(new UsedRefreshToken(1, 'refresh-a')); + + $this->assertTrue($repo->exists('refresh-a')); + } + + public function testSaveOverwritesPriorEntryForSameRefreshTokenString(): void + { + $repo = new InMemoryUsedRefreshTokenRepository([ + new UsedRefreshToken(1, 'refresh-a'), + ]); + $repo->save(new UsedRefreshToken(2, 'refresh-a')); + + $this->assertTrue($repo->exists('refresh-a')); + // deleting by the old tokenId should not remove the rebound entry + $repo->deleteAllForTokenId(1); + $this->assertTrue($repo->exists('refresh-a')); + } + + public function testDeleteAllForTokenId(): void + { + $repo = new InMemoryUsedRefreshTokenRepository([ + new UsedRefreshToken(1, 'refresh-a'), + new UsedRefreshToken(1, 'refresh-b'), + new UsedRefreshToken(2, 'refresh-c'), + ]); + + $repo->deleteAllForTokenId(1); + + $this->assertFalse($repo->exists('refresh-a')); + $this->assertFalse($repo->exists('refresh-b')); + $this->assertTrue($repo->exists('refresh-c')); + } +} diff --git a/tests/Infrastructure/Persistence/Record/InMemoryBacklinkRepositoryTest.php b/tests/Infrastructure/Persistence/Record/InMemoryBacklinkRepositoryTest.php new file mode 100644 index 0000000..2889047 --- /dev/null +++ b/tests/Infrastructure/Persistence/Record/InMemoryBacklinkRepositoryTest.php @@ -0,0 +1,69 @@ +assertCount(1, $repo->findByUri('at://alice/like/1')); + $this->assertCount(2, $repo->findByLinkTo('at://bob/post/1')); + $this->assertCount(0, $repo->findByUri('nope')); + } + + public function testSaveIsIdempotentForDuplicateTriple(): void + { + $repo = new InMemoryBacklinkRepository(); + $bl = new Backlink('at://alice/like/1', 'subject.uri', 'at://bob/post/1'); + + $repo->save($bl); + $repo->save($bl); + $repo->save(new Backlink('at://alice/like/1', 'subject.uri', 'at://bob/post/1')); + + $this->assertCount(1, $repo->findByUri('at://alice/like/1')); + } + + public function testSaveAddsWhenPathOrLinkToDiffers(): void + { + $repo = new InMemoryBacklinkRepository(); + $repo->save(new Backlink('at://alice/like/1', 'subject.uri', 'at://bob/post/1')); + $repo->save(new Backlink('at://alice/like/1', 'reply.parent.uri', 'at://bob/post/1')); + + $this->assertCount(2, $repo->findByUri('at://alice/like/1')); + } + + public function testDeleteByUri(): void + { + $repo = new InMemoryBacklinkRepository([ + new Backlink('at://alice/like/1', 'subject.uri', 'at://bob/post/1'), + new Backlink('at://alice/like/2', 'subject.uri', 'at://bob/post/1'), + ]); + + $repo->deleteByUri('at://alice/like/1'); + + $this->assertCount(0, $repo->findByUri('at://alice/like/1')); + $this->assertCount(1, $repo->findByLinkTo('at://bob/post/1')); + } + + public function testDeleteAll(): void + { + $repo = new InMemoryBacklinkRepository([ + new Backlink('at://alice/like/1', 'subject.uri', 'at://bob/post/1'), + ]); + + $repo->deleteAll(); + + $this->assertCount(0, $repo->findByLinkTo('at://bob/post/1')); + } +} diff --git a/tests/Infrastructure/Persistence/Record/InMemoryRecordBlobRepositoryTest.php b/tests/Infrastructure/Persistence/Record/InMemoryRecordBlobRepositoryTest.php new file mode 100644 index 0000000..e1e05ac --- /dev/null +++ b/tests/Infrastructure/Persistence/Record/InMemoryRecordBlobRepositoryTest.php @@ -0,0 +1,62 @@ +assertCount(2, $repo->findByBlobCid('blob-1')); + $this->assertCount(2, $repo->findByRecordUri('at://alice/post/1')); + $this->assertCount(0, $repo->findByBlobCid('nope')); + } + + public function testSaveIsIdempotent(): void + { + $repo = new InMemoryRecordBlobRepository(); + $repo->save(new RecordBlob('blob-1', 'at://alice/post/1')); + $repo->save(new RecordBlob('blob-1', 'at://alice/post/1')); + + $this->assertCount(1, $repo->findByBlobCid('blob-1')); + } + + public function testDeleteByRecordUri(): void + { + $repo = new InMemoryRecordBlobRepository([ + new RecordBlob('blob-1', 'at://alice/post/1'), + new RecordBlob('blob-2', 'at://alice/post/1'), + new RecordBlob('blob-1', 'at://alice/post/2'), + ]); + + $repo->deleteByRecordUri('at://alice/post/1'); + + $this->assertCount(0, $repo->findByRecordUri('at://alice/post/1')); + $this->assertCount(1, $repo->findByRecordUri('at://alice/post/2')); + } + + public function testDeleteByBlobCid(): void + { + $repo = new InMemoryRecordBlobRepository([ + new RecordBlob('blob-1', 'at://alice/post/1'), + new RecordBlob('blob-1', 'at://alice/post/2'), + new RecordBlob('blob-2', 'at://alice/post/3'), + ]); + + $repo->deleteByBlobCid('blob-1'); + + $this->assertCount(0, $repo->findByBlobCid('blob-1')); + $this->assertCount(1, $repo->findByBlobCid('blob-2')); + } +} -- 2.51.2