登录
ZH
apps/caramail-server / tests / ChatServerTest.php · commit 070afda
<?phpnamespace Tests;use App\Chat\ChatServer;use App\Chat\Frame;use App\Chat\MemorySession;use App\Chat\RoomStore;use App\Chat\TempId;use PHPUnit\Framework\TestCase;/** The chat's protocol, frame by frame, on a server held in memory (as the 2002 applet speaks it). */final class ChatServerTest extends TestCase{    private string $file;    private float $now = 1000.0;    private ChatServer $chat;    protected function setUp(): void    {        $this->file = sys_get_temp_dir() . '/caramail-test-' . bin2hex(random_bytes(4)) . '/rooms.json';        $this->chat = new ChatServer(new RoomStore($this->file), 'secret', null, fn () => $this->now);    }    protected function tearDown(): void    {        @unlink($this->file);        @rmdir(dirname($this->file));    }    private function login(string $nickname, int $age = 25, int $sex = 0, string $city = 'Paris', string $key = ''): MemorySession    {        $s = new MemorySession($nickname);        $this->chat->connect($s);        $this->chat->receive($s, '0', ['0', TempId::make($nickname, $age, $sex, $city, $key)]);        return $s;    }    public function testANewClientIsGreetedWithTheBanner(): void    {        $s = new MemorySession();        $this->chat->connect($s);        self::assertSame(["100\t" . ChatServer::BANNER], $s->received);    }    public function testTheLoginAnswersTheIdTheNicknameAndTheConnectedList(): void    {        $s = $this->login('Alice', 24, 1, 'Lyon');        [$id, $code, $zero, $nickname] = explode("\t", $s->frames('0')[0]);        self::assertMatchesRegularExpression('/^\d+:\d+:\d+:0$/', $id);        self::assertSame(['197', '0', 'Alice'], [$code, $zero, $nickname]);        self::assertSame(['-1'], $s->frames('330'));        self::assertSame("-2:-2:-2\t$id\tAlice\t0\t24\t1\tLyon\t?\t2\t", $s->frames('56')[0]);    }    public function testABadTempidIsDropped(): void    {        $s = new MemorySession();        $this->chat->connect($s);        $this->chat->receive($s, '0', ['0', '']);        self::assertTrue($s->closed);        self::assertSame([], $this->chat->users());    }    public function testAFrameBeforeTheLoginIsDropped(): void    {        $s = new MemorySession();        $this->chat->connect($s);        $this->chat->receive($s, '70', ['0100hello']);        self::assertTrue($s->closed);    }    public function testTwoMembersCannotHaveTheSameNickname(): void    {        $this->login('Bob');        $second = $this->login('bob');        self::assertSame('bob_2', explode("\t", $second->frames('0')[0])[3]);    }    public function testEnteringARoomTellsItsMembersAndSendsItsLastSentences(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '20', ['Salon', '0']);        $this->chat->receive($alice, '70', ['0100Bonjour']);        $alice->take();        $this->chat->receive($bob, '20', ['Salon', '0']);        $bobId = $this->idOf($bob);        self::assertSame(["0\t$bobId\t0\tBob\t0\t25\t0\tParis\t"], $alice->frames('85'));        self::assertSame(["0\tSalon\t?\tadmin\t"], $bob->frames('20'));        self::assertSame([$this->idOf($alice) . "\t0\tAlice\t0100Bonjour\t"], $bob->frames('70'));    }    public function testASentenceGoesToEveryMemberOfTheRoomOnly(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $carol = $this->login('Carol');        $this->chat->receive($alice, '20', ['Musique', '0']);        $this->chat->receive($bob, '20', ['Musique', '0']);        $this->chat->receive($carol, '20', ['Sport', '0']);        foreach ([$alice, $bob, $carol] as $s) {            $s->take();        }        $this->chat->receive($alice, '70', ['0302Salut :-)']);        $line = $this->idOf($alice) . "\t0\tAlice\t0302Salut :-)\t";        self::assertSame([$line], $alice->frames('70'));        self::assertSame([$line], $bob->frames('70'));        self::assertSame([], $carol->frames('70'));    }    public function testLeavingARoomTellsTheOthersAndAnEmptyRoomDisappears(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '20', ['Ma salle', '0']);        $this->chat->receive($bob, '20', ['Ma salle', '0']);        $bob->take();        $this->chat->receive($alice, '20', ['Salon', '0']);        self::assertSame(["1\t" . $this->idOf($alice) . "\t0\tAlice\t0\t25\t0\tParis\t"], $bob->frames('85'));        $this->chat->disconnect($bob);        self::assertNull($this->chat->room('Ma salle'));        self::assertNotNull($this->chat->room('Salon'));    }    public function testAFullRoomOf32SendsTheNextOnesToNumberedRooms(): void    {        for ($i = 0; $i < 64; $i++) {            $this->chat->receive($this->login("m$i"), '20', ['Foule', '0']);        }        self::assertSame(32, $this->chat->room('Foule')->count());        self::assertSame(32, $this->chat->room('Foule#1')->count());        $late = $this->login('Late');        $this->chat->receive($late, '20', ['Foule', '0']);        self::assertSame(["0\tFoule#2\t?\tLate\t"], $late->frames('20'));    }    public function testThePermanentRoomsAreListedWithTheirMembers(): void    {        $alice = $this->login('Alice');        $this->chat->receive($alice, '20', ['Salon', '0']);        $this->chat->receive($alice, '20', ['Salon', '0']);        $this->chat->receive($alice, '94', []);        self::assertStringStartsWith("Salon\t?\tadmin\t1\t10 - 15 ans\t?\tadmin\t0\t", $alice->frames('94')[0]);        $this->chat->receive($alice, '20', ['Chez moi', '0']);        $this->chat->receive($alice, '90', []);        self::assertSame(["Chez moi\t?\tAlice\t1\t"], $alice->frames('90'));    }    public function testAPrivateMessageReachesItsReceiverAndEchoesToTheSender(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $alice->take();        $bob->take();        $this->chat->receive($alice, '72', [$this->idOf($bob), '0100Coucou']);        self::assertSame([$this->idOf($bob) . "\t0\tBob\t0100Coucou\t"], $alice->frames('73'));        self::assertSame([$this->idOf($alice) . "\t0\tAlice\t0100Coucou\t"], $bob->frames('72'));    }    private function idOf(MemorySession $s): string    {        foreach ($this->chat->users() as $user) {            if ($user->session === $s) {                return $user->id;            }        }        self::fail('no user');    }    public function testTheProfileOfAMemberIsGiven(): void    {        $alice = $this->login('Alice', 31, 1, 'Nantes');        $bob = $this->login('Bob');        $this->chat->receive($bob, '82', [$this->idOf($alice)]);        self::assertSame([$this->idOf($alice) . "\t0\t31\t1\tNantes\t"], $bob->frames('82'));    }    public function testTheSearchFiltersByNicknameCitySexAndAge(): void    {        $this->login('Alice', 24, 1, 'Lyon');        $this->login('Albert', 52, 0, 'Lille');        $me = $this->login('Zoe', 30, 1, 'Paris');        $me->take();        $this->chat->receive($me, '56', ['al', '-2', '-2']);        self::assertStringContainsString("\tAlice\t", $me->frames('56')[0]);        self::assertStringContainsString("\tAlbert\t", $me->frames('56')[0]);        $this->chat->receive($me, '56', ['al', '-2', '1']);        self::assertStringStartsWith("-2:1:1\t", $me->frames('56')[1]);        self::assertStringNotContainsString('Albert', $me->frames('56')[1]);        $this->chat->receive($me, '58', ['li', '-2', '-2']);        self::assertStringContainsString("\tAlbert\t", $me->frames('58')[0]);        self::assertStringNotContainsString('Alice', $me->frames('58')[0]);        $this->chat->receive($me, '56', ['', '10', '-2']);    // 70 and more: nobody        self::assertSame("10:-2\t", $me->frames('56')[2]);    }    public function testTheAgeBandsOfTheApplet(): void    {        self::assertSame([10, 15], ChatServer::ageRange(2));        self::assertSame([30, 40], ChatServer::ageRange(6));        self::assertSame([50, 60], ChatServer::ageRange(8));        self::assertSame([70, PHP_INT_MAX], ChatServer::ageRange(10));    }    public function testTheNumberOfConnectedMembers(): void    {        $this->login('A');        $b = $this->login('B');        $this->chat->receive($b, '199', []);        self::assertSame(['2'], $b->frames('199'));    }    public function testAModeratorEjectsSomebodyAndTheRoomIsTold(): void    {        $modo = $this->login('Modo', key: 'secret');        $bad = $this->login('Troll');        $this->chat->receive($modo, '20', ['Salon', '0']);        $this->chat->receive($bad, '20', ['Salon', '0']);        $modo->take();        $badId = $this->idOf($bad);        $this->chat->receive($modo, '555', ['Troll:0']);        self::assertSame([$this->idOf($modo) . "\tModo\t"], $bad->frames('555'));        self::assertSame(["5\t" . $badId . "\tTroll\t" . $this->idOf($modo) . "\tModo\t"], $modo->frames('76'));        self::assertTrue($bad->closed, 'the applet ends its session on 555');        self::assertCount(1, $this->chat->room('Salon')->members);    }    public function testAMemberCannotEjectSomebodyElse(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '555', ['Bob:0']);        self::assertSame([], $bob->frames('555'));    }    public function testAModeratorUpdateReachesEverybody(): void    {        $modo = $this->login('Modo', key: 'secret');        $other = $this->login('Other');        $this->chat->receive($modo, '20', ['Salon', '0']);        $this->chat->receive($modo, '70', ['0103!update Maintenance à 22h']);        self::assertSame(['0503Maintenance à 22h'], $other->frames('77'));        $this->chat->receive($other, '20', ['Salon', '0']);        $this->chat->receive($other, '70', ['0100!update pirate']);        self::assertCount(1, $other->frames('77'));    }    public function testQuietClientsArePingedAndSilentOnesDropped(): void    {        $s = $this->login('Sleepy');        $this->now += ChatServer::PING_EVERY + 1;        $this->chat->tick();        self::assertCount(1, $s->frames('667'));        $this->now += ChatServer::TIMEOUT;        $this->chat->tick();        self::assertTrue($s->closed);        self::assertSame([], $this->chat->users());    }    public function testThePermanentRoomsAndTheirSentencesSurviveARestart(): void    {        $alice = $this->login('Alice');        $this->chat->receive($alice, '20', ['Salon', '0']);        $this->chat->receive($alice, '70', ['0100Je reviens']);        $this->chat->shutdown();        $again = new ChatServer(new RoomStore($this->file));        self::assertSame('0100Je reviens', $again->room('Salon')->history[0]['text']);        self::assertSame(0, $again->room('Salon')->count());    }    public function testAnEmptyCityIsSentAsAQuestionMarkForTheAppletsTokenizer(): void    {        $s = $this->login('Nobody', 30, 1, '');        self::assertStringContainsString("\tNobody\t0\t30\t1\t?\t", $s->frames('56')[0]);    }    public function testAMemberWhoRefusesPrivateTalksAnswers75(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($bob, '26', ['2']);        self::assertSame(['2'], $bob->frames('26'));        $this->chat->receive($alice, '72', [$this->idOf($bob), '0100Coucou']);        self::assertSame([$this->idOf($bob) . "\tBob"], $alice->frames('75'));        self::assertSame([], $bob->frames('72'));    }    public function testAPrivateMessageCanNameItsReceiverByNicknameAndOrigin(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '72', ['bob:0', '0100Psst']);        self::assertCount(1, $bob->frames('72'));    }    public function testThePrivatePartnersHear74WhenSomebodyLeaves(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '72', [$this->idOf($bob), '0100Salut']);        $aliceId = $this->idOf($alice);        $this->chat->disconnect($alice);        self::assertSame([$aliceId], $bob->frames('74'));    }    public function testTheIgnoreListIsKeptAndSentBack(): void    {        $alice = $this->login('Alice');        $this->chat->receive($alice, '18', ['0', 'Troll:0']);        $this->chat->receive($alice, '18', ['0', 'Pest:0']);        $this->chat->receive($alice, '18', ['1', 'Troll:0']);        self::assertSame("-1\tPest:0", $alice->frames('18')[2]);    }    public function testCallingAModeratorReachesTheModerators(): void    {        $modo = $this->login('Modo', key: 'secret');        $alice = $this->login('Alice');        $this->chat->receive($alice, '20', ['Salon', '0']);        $this->chat->receive($alice, '550', []);        self::assertSame(["Salon\t1"], $modo->frames('550'), 'one member signals a problem in Salon');        self::assertSame(['1'], $alice->frames('550'), 'the caller: "Un modérateur a été prévenu"');        $this->chat->disconnect($modo);        $this->chat->receive($alice, '550', []);        self::assertCount(1, $alice->frames('551'), 'no moderator: "Aucun modérateur n\'est joignable"');    }    public function testTheRoomTellsWhoComesAndGoesAsTheAppletsInformationLines(): void    {        $alice = $this->login('Alice');        $bob = $this->login('Bob');        $this->chat->receive($alice, '20', ['Salon', '0']);        self::assertSame(["1\tSalon"], $alice->frames('76'), '"Bienvenue dans le salon"');        $this->chat->receive($bob, '20', ['Salon', '0']);        $this->chat->receive($bob, '20', ['Musique', '0']);        $this->chat->receive($bob, '20', ['Salon', '0']);        $this->chat->disconnect($bob);        self::assertSame(["1\tSalon", "2\tBob", "3\tBob", "2\tBob", "4\tBob"], $alice->frames('76'));    }    public function testFramesAreCutAtLineEndsAndKeepTheUnfinishedOne(): void    {        $buffer = "70\t0100a\n667\n\n20\tAccu";        self::assertSame([['70', ['0100a']], ['667', []]], Frame::decode($buffer));        self::assertSame("20\tAccu", $buffer);        self::assertSame("667\t\n", Frame::encode('667'));        self::assertSame("70\ta b\n", Frame::encode('70', ["a\nb"]));    }    /** The level the server gave somebody: its profile (82) as another member reads it. */    private function levelOf(MemorySession $who): string    {        $reader = $this->login('Reader' . random_int(100, 999));        $id = explode("\t", $who->frames('0')[0])[0];        $this->chat->receive($reader, '82', [$id]);        return explode("\t", $reader->frames('82')[0])[1];    }    private function loginFrom(string $ip, string $nickname, string $extra): MemorySession    {        $s = new MemorySession($nickname);        $s->ip = $ip;        $this->chat->connect($s);        $this->chat->receive($s, '0', ['0', TempId::make($nickname, 30, 1, 'Paris') . $extra]);        return $s;    }    public function testTheAdministratorKeyGivesTheSecondLevelAndTheModeratorKeyTheFirst(): void    {        $this->chat->setRoles('super', []);        self::assertSame('2', $this->levelOf($this->login('Boss', key: 'super')));        self::assertSame('1', $this->levelOf($this->login('Modo', key: 'secret')));        self::assertSame('0', $this->levelOf($this->login('Bob', key: 'wrong')));    }    public function testTheOwnersAddressChoosesItsRoleAndNobodyElseCan(): void    {        $this->chat->setRoles('', ['82.67.98.164']);        self::assertSame('2', $this->levelOf($this->loginFrom('82.67.98.164', 'Owner', '&r=2')));        self::assertSame('1', $this->levelOf($this->loginFrom('82.67.98.164', 'OwnerModo', '&r=1')));        self::assertSame('0', $this->levelOf($this->loginFrom('82.67.98.164', 'OwnerPlain', '&r=0')));        self::assertSame('0', $this->levelOf($this->loginFrom('1.2.3.4', 'Stranger', '&r=2')));    }    public function testTheLoginFormLearnsWhetherItIsTheOwnersAddress(): void    {        $this->chat->setRoles('', ['82.67.98.164']);        foreach (['82.67.98.164' => '1', '1.2.3.4' => '0'] as $ip => $answer) {            $s = new MemorySession();            $s->ip = $ip;            $this->chat->connect($s);            $this->chat->receive($s, '700', []);            self::assertSame([$answer], $s->frames('700'));            self::assertFalse($s->closed);        }    }}