782 lines
30 KiB
PHP
782 lines
30 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Channel;
|
|
use App\Models\PageVisit;
|
|
use App\Models\User;
|
|
use App\Services\TransferService;
|
|
use App\Telegram\TelegramBotContext;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use SergiX44\Nutgram\Nutgram;
|
|
use SergiX44\Nutgram\Telegram\Properties\ChatMemberStatus;
|
|
use SergiX44\Nutgram\Telegram\Properties\ChatType;
|
|
use SergiX44\Nutgram\Telegram\Properties\UpdateType;
|
|
use SergiX44\Nutgram\Testing\FakeNutgram;
|
|
use Tests\TestCase;
|
|
|
|
class TelegramBotTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const OWNER_CHAT = -1001234567890;
|
|
|
|
private const AGENT_CHAT = -1009876543210;
|
|
|
|
private const USER_ID = 4242;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
config([
|
|
'coruna.telegram.owner_chat_id' => (string) self::OWNER_CHAT,
|
|
'coruna.telegram.webhook_secret' => 'test-secret',
|
|
'nutgram.token' => FakeNutgram::TOKEN,
|
|
]);
|
|
|
|
app(TelegramBotContext::class)->setAgent(null);
|
|
|
|
// Nutgram is a singleton; reset so FakeNutgram mock queues stay isolated.
|
|
$this->app->forgetInstance(Nutgram::class);
|
|
$this->app->forgetInstance('nutgram');
|
|
$this->app->forgetInstance('telegram');
|
|
$this->app->forgetInstance(FakeNutgram::class);
|
|
}
|
|
|
|
#[Test]
|
|
public function webhook_rejects_bad_secret(): void
|
|
{
|
|
$this->postJson('/hooks/telegram', ['update_id' => 1], [
|
|
'X-Telegram-Bot-Api-Secret-Token' => 'wrong',
|
|
])->assertForbidden();
|
|
}
|
|
|
|
#[Test]
|
|
public function webhook_accepts_valid_secret(): void
|
|
{
|
|
$this->call(
|
|
'POST',
|
|
'/hooks/telegram',
|
|
[],
|
|
[],
|
|
[],
|
|
[
|
|
'CONTENT_TYPE' => 'application/json',
|
|
'HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN' => 'test-secret',
|
|
],
|
|
json_encode(['update_id' => 1])
|
|
)->assertNoContent();
|
|
}
|
|
|
|
#[Test]
|
|
public function webhook_acks_even_when_bot_run_fails(): void
|
|
{
|
|
$this->app->bind(Nutgram::class, function () {
|
|
throw new \RuntimeException('deliberate nutgram failure');
|
|
});
|
|
|
|
$this->call(
|
|
'POST',
|
|
'/hooks/telegram',
|
|
[],
|
|
[],
|
|
[],
|
|
[
|
|
'CONTENT_TYPE' => 'application/json',
|
|
'HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN' => 'test-secret',
|
|
],
|
|
json_encode(['update_id' => 99, 'message' => ['text' => '/ping']])
|
|
)->assertNoContent();
|
|
}
|
|
|
|
#[Test]
|
|
public function unauthorized_chat_is_silent(): void
|
|
{
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
foreach (['/ping', '/help', '/data', '/channel', '/transfer Txxx USDT 1'] as $text) {
|
|
$this->app->forgetInstance(Nutgram::class);
|
|
$this->app->forgetInstance('nutgram');
|
|
$this->app->forgetInstance('telegram');
|
|
$this->app->forgetInstance(FakeNutgram::class);
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->hearMessage([
|
|
'text' => $text,
|
|
'chat' => ['id' => 999, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 0);
|
|
}
|
|
}
|
|
|
|
#[Test]
|
|
public function private_chat_is_silent_even_with_matching_id(): void
|
|
{
|
|
config(['coruna.telegram.owner_chat_id' => (string) self::USER_ID]);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/help',
|
|
'chat' => ['id' => self::USER_ID, 'type' => ChatType::PRIVATE->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 0);
|
|
}
|
|
|
|
#[Test]
|
|
public function non_admin_is_rejected(): void
|
|
{
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::MEMBER->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'],
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/help',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'X'],
|
|
])->reply();
|
|
|
|
// index 0 is getChatMember; reject reply follows
|
|
$bot->assertReplyText('⛔ Only group admins can use this command.', 1);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_help_lists_commands(): void
|
|
{
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/help',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 1);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$text = $last['text'] ?? '';
|
|
$this->assertStringContainsString('/help', $text);
|
|
$this->assertStringContainsString('/data', $text);
|
|
$this->assertStringContainsString('/channel', $text);
|
|
$this->assertStringContainsString('/transfer', $text);
|
|
$this->assertStringContainsString('/ping', $text);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_ping_replies_pong(): void
|
|
{
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/ping',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
])->reply();
|
|
|
|
$bot->assertReplyText('pong', 1);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_transfer_calls_transfer_service(): void
|
|
{
|
|
$mock = Mockery::mock(TransferService::class);
|
|
$mock->shouldReceive('handle')
|
|
->once()
|
|
->withArgs(function (...$args) {
|
|
return ($args[0] ?? null) === 'tron'
|
|
&& ($args[1] ?? null) === 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH'
|
|
&& ($args[2] ?? null) === '10'
|
|
&& ($args[3] ?? null) === 'USDT'
|
|
&& is_string($args[4] ?? null)
|
|
&& str_starts_with((string) $args[4], 'telegram:');
|
|
})
|
|
->andReturn([
|
|
'ok' => true,
|
|
'txid' => 'deadbeef',
|
|
'from' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
|
'to' => 'TPayoutAddressxxxxxxxxxxxxxxxxxxxxxx',
|
|
'amount' => '10',
|
|
'asset' => 'USDT',
|
|
]);
|
|
$this->app->instance(TransferService::class, $mock);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::CREATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'],
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 10',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Owner'],
|
|
])->reply();
|
|
|
|
// getChatMember + "⏳ …" + success
|
|
$bot->assertCalled('sendMessage', 2);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString('deadbeef', $last['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_data_reports_dashboard_stats(): void
|
|
{
|
|
$channel = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
PageVisit::query()->create([
|
|
'channel_id' => $channel,
|
|
'client_uid' => 'u1',
|
|
'os' => 'iOS',
|
|
'os_version' => '16.6',
|
|
'browser' => 'Safari',
|
|
'created_at' => now(),
|
|
]);
|
|
PageVisit::query()->create([
|
|
'channel_id' => $channel,
|
|
'client_uid' => 'u2',
|
|
'os' => 'Android',
|
|
'os_version' => '13',
|
|
'browser' => 'Chrome',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/data 1 '.$channel,
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 1);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$text = $last['text'] ?? '';
|
|
$this->assertStringContainsString('页面 PV: 1 / 2', $text);
|
|
$this->assertStringContainsString('设备与资产', $text);
|
|
$this->assertStringContainsString('总余额', $text);
|
|
$this->assertStringContainsString('交易额', $text);
|
|
$this->assertStringContainsString('助记词', $text);
|
|
$this->assertStringContainsString($channel, $text);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_channel_lists_copyable_links(): void
|
|
{
|
|
config(['coruna.channel_domains' => ['example.com']]);
|
|
|
|
Channel::query()->create([
|
|
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
'user_id' => Channel::OFFICIAL_USER_ID,
|
|
'remark' => '主站',
|
|
'status' => 1,
|
|
]);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/channel',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 1);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', $last['text'] ?? '');
|
|
$this->assertStringContainsString('主站', $last['text'] ?? '');
|
|
$this->assertStringContainsString('官方', $last['text'] ?? '');
|
|
$this->assertArrayHasKey('reply_markup', $last);
|
|
}
|
|
|
|
#[Test]
|
|
public function admin_channel_new_builder_copies_promo_iframe(): void
|
|
{
|
|
config([
|
|
'coruna.channel_domains' => ['cdn.example.com'],
|
|
'coruna.static_site.scheme' => 'https',
|
|
]);
|
|
|
|
Channel::query()->create([
|
|
'channel_id' => 'S.U.N2',
|
|
'builder_type' => Channel::BUILDER_NEW,
|
|
'user_id' => Channel::OFFICIAL_USER_ID,
|
|
'remark' => 'SUN02',
|
|
'status' => 1,
|
|
]);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/channel',
|
|
'chat' => ['id' => self::OWNER_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 1);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString('S.U.N2', $last['text'] ?? '');
|
|
$this->assertStringContainsString('推广 iframe', $last['text'] ?? '');
|
|
|
|
$markup = $last['reply_markup'] ?? null;
|
|
$this->assertIsArray($markup);
|
|
$row = $markup['inline_keyboard'][0][0] ?? null;
|
|
$this->assertIsArray($row);
|
|
$this->assertStringContainsString('推广代码', (string) ($row['text'] ?? ''));
|
|
$copy = (string) data_get($row, 'copy_text.text', '');
|
|
$this->assertLessThanOrEqual(256, strlen($copy));
|
|
$this->assertStringContainsString('/channel/S.U.N2/weifile/weifile.html', $copy);
|
|
$this->assertStringNotContainsString('https://cdn.example.com', $copy);
|
|
$this->assertStringContainsString('pointer-events:none', $copy);
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_bot_only_sees_own_channels_and_stats(): void
|
|
{
|
|
$agentA = User::query()->create([
|
|
'username' => 'agent_a',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => (string) self::AGENT_CHAT,
|
|
]);
|
|
$agentB = User::query()->create([
|
|
'username' => 'agent_b',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => '-100111',
|
|
]);
|
|
|
|
$chA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
$chB = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
|
Channel::query()->create(['channel_id' => $chA, 'user_id' => $agentA->id, 'status' => 1, 'remark' => 'A站']);
|
|
Channel::query()->create(['channel_id' => $chB, 'user_id' => $agentB->id, 'status' => 1, 'remark' => 'B站']);
|
|
|
|
PageVisit::query()->create([
|
|
'channel_id' => $chA,
|
|
'client_uid' => 'ua',
|
|
'os' => 'iOS',
|
|
'os_version' => '16.0',
|
|
'browser' => 'Safari',
|
|
'created_at' => now(),
|
|
]);
|
|
PageVisit::query()->create([
|
|
'channel_id' => $chB,
|
|
'client_uid' => 'ub',
|
|
'os' => 'iOS',
|
|
'os_version' => '16.0',
|
|
'browser' => 'Safari',
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/channel',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history = $bot->getRequestHistory();
|
|
$channelMsg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString($chA, $channelMsg['text'] ?? '');
|
|
$this->assertStringNotContainsString($chB, $channelMsg['text'] ?? '');
|
|
|
|
$this->app->forgetInstance(Nutgram::class);
|
|
$this->app->forgetInstance('nutgram');
|
|
$this->app->forgetInstance('telegram');
|
|
$this->app->forgetInstance(FakeNutgram::class);
|
|
app(TelegramBotContext::class)->setAgent($agentA);
|
|
|
|
/** @var FakeNutgram $bot2 */
|
|
$bot2 = app(Nutgram::class);
|
|
$bot2->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
$bot2->hearMessage([
|
|
'text' => '/data 1',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history2 = $bot2->getRequestHistory();
|
|
$dataMsg = FakeNutgram::getActualData(array_values($history2[array_key_last($history2)])[0]);
|
|
$this->assertStringContainsString('页面 PV: 1 / 1', $dataMsg['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_bot_rejects_foreign_channel_and_transfer(): void
|
|
{
|
|
$agent = User::query()->create([
|
|
'username' => 'agent_x',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => (string) self::AGENT_CHAT,
|
|
]);
|
|
Channel::query()->create([
|
|
'channel_id' => 'cccccccccccccccccccccccccccccccc',
|
|
'user_id' => $agent->id,
|
|
'status' => 1,
|
|
]);
|
|
$foreign = 'dddddddddddddddddddddddddddddddd';
|
|
Channel::query()->create([
|
|
'channel_id' => $foreign,
|
|
'user_id' => Channel::OFFICIAL_USER_ID,
|
|
'status' => 1,
|
|
]);
|
|
|
|
app(TelegramBotContext::class)->setAgent($agent);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
|
|
$bot->hearMessage([
|
|
'text' => '/data 1 '.$foreign,
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history = $bot->getRequestHistory();
|
|
$msg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString('无权查看该渠道', $msg['text'] ?? '');
|
|
|
|
$this->app->forgetInstance(Nutgram::class);
|
|
$this->app->forgetInstance('nutgram');
|
|
$this->app->forgetInstance('telegram');
|
|
$this->app->forgetInstance(FakeNutgram::class);
|
|
app(TelegramBotContext::class)->setAgent($agent);
|
|
|
|
/** @var FakeNutgram $bot2 */
|
|
$bot2 = app(Nutgram::class);
|
|
$bot2->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
$bot2->hearMessage([
|
|
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history2 = $bot2->getRequestHistory();
|
|
$transferMsg = FakeNutgram::getActualData(array_values($history2[array_key_last($history2)])[0]);
|
|
$this->assertStringContainsString('无权操作该地址', $transferMsg['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_help_lists_transfer_for_own_devices(): void
|
|
{
|
|
$agent = User::query()->create([
|
|
'username' => 'agent_help',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => (string) self::AGENT_CHAT,
|
|
]);
|
|
app(TelegramBotContext::class)->setAgent($agent);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
$bot->hearMessage([
|
|
'text' => '/help',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history = $bot->getRequestHistory();
|
|
$msg = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString('/data', $msg['text'] ?? '');
|
|
$this->assertStringContainsString('/transfer <fromAddress>', $msg['text'] ?? '');
|
|
$this->assertStringNotContainsString('仅可查看/转出本代理渠道设备', $msg['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_can_transfer_own_device_address(): void
|
|
{
|
|
$agent = User::query()->create([
|
|
'username' => 'agent_xfer',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => (string) self::AGENT_CHAT,
|
|
]);
|
|
$channelId = 'cccccccccccccccccccccccccccccccc';
|
|
Channel::query()->create([
|
|
'channel_id' => $channelId,
|
|
'user_id' => $agent->id,
|
|
'status' => 1,
|
|
]);
|
|
$device = \App\Models\Device::query()->create([
|
|
'device_id' => 'dev-agent-xfer',
|
|
'channel_id' => $channelId,
|
|
]);
|
|
\App\Models\WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
]);
|
|
|
|
$mock = Mockery::mock(TransferService::class);
|
|
$mock->shouldReceive('handle')
|
|
->once()
|
|
->andReturn([
|
|
'ok' => true,
|
|
'txid' => 'agentbeef',
|
|
'from' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
|
'to' => 'TPayoutAddressxxxxxxxxxxxxxxxxxxxxxx',
|
|
'amount' => '1',
|
|
'asset' => 'USDT',
|
|
]);
|
|
$this->app->instance(TransferService::class, $mock);
|
|
|
|
app(TelegramBotContext::class)->setAgent($agent);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
$bot->hearMessage([
|
|
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$history = $bot->getRequestHistory();
|
|
$messages = $this->sendMessagePayloads($history);
|
|
$this->assertCount(3, $messages);
|
|
$this->assertStringContainsString('agentbeef', $messages[1]['text'] ?? '');
|
|
$this->assertSame((string) self::OWNER_CHAT, (string) ($messages[2]['chat_id'] ?? ''));
|
|
$this->assertStringContainsString('代理转账 · agent_xfer', $messages[2]['text'] ?? '');
|
|
$this->assertStringContainsString('agentbeef', $messages[2]['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_transfer_failure_is_copied_to_owner_chat(): void
|
|
{
|
|
$agent = User::query()->create([
|
|
'username' => 'agent_xfer_fail',
|
|
'password' => 'secret12',
|
|
'status' => 1,
|
|
'chat_id' => (string) self::AGENT_CHAT,
|
|
]);
|
|
$channelId = 'cccccccccccccccccccccccccccccccc';
|
|
Channel::query()->create([
|
|
'channel_id' => $channelId,
|
|
'user_id' => $agent->id,
|
|
'status' => 1,
|
|
]);
|
|
$device = \App\Models\Device::query()->create([
|
|
'device_id' => 'dev-agent-xfer-fail',
|
|
'channel_id' => $channelId,
|
|
]);
|
|
\App\Models\WalletAddress::query()->create([
|
|
'device_id' => $device->id,
|
|
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
|
'chain_type' => 'TRON',
|
|
'source' => 'imToken',
|
|
]);
|
|
|
|
$mock = Mockery::mock(TransferService::class);
|
|
$mock->shouldReceive('handle')
|
|
->once()
|
|
->andReturn([
|
|
'ok' => false,
|
|
'error' => 'insufficient energy',
|
|
]);
|
|
$this->app->instance(TransferService::class, $mock);
|
|
|
|
app(TelegramBotContext::class)->setAgent($agent);
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->willReceivePartial([
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
'can_manage_chat' => true,
|
|
]);
|
|
$bot->hearMessage([
|
|
'text' => '/transfer TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH USDT 1',
|
|
'chat' => ['id' => self::AGENT_CHAT, 'type' => ChatType::SUPERGROUP->value],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'AgentAdmin'],
|
|
])->reply();
|
|
|
|
$messages = $this->sendMessagePayloads($bot->getRequestHistory());
|
|
$this->assertCount(3, $messages);
|
|
$this->assertStringContainsString('insufficient energy', $messages[1]['text'] ?? '');
|
|
$this->assertSame((string) self::OWNER_CHAT, (string) ($messages[2]['chat_id'] ?? ''));
|
|
$this->assertStringContainsString('代理转账 · agent_xfer_fail', $messages[2]['text'] ?? '');
|
|
$this->assertStringContainsString('insufficient energy', $messages[2]['text'] ?? '');
|
|
}
|
|
|
|
#[Test]
|
|
public function agent_webhook_route_is_removed(): void
|
|
{
|
|
$this->postJson('/hooks/telegram/agent/1', ['update_id' => 1])
|
|
->assertNotFound();
|
|
}
|
|
|
|
#[Test]
|
|
public function joining_a_group_announces_chat_id(): void
|
|
{
|
|
$chatId = -1005556667778;
|
|
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
$bot->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [
|
|
'chat' => [
|
|
'id' => $chatId,
|
|
'type' => ChatType::SUPERGROUP->value,
|
|
'title' => '代理测试群',
|
|
],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'date' => time(),
|
|
'old_chat_member' => [
|
|
'status' => ChatMemberStatus::LEFT->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
'new_chat_member' => [
|
|
'status' => ChatMemberStatus::MEMBER->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
])->reply();
|
|
|
|
$bot->assertCalled('sendMessage', 1);
|
|
$history = $bot->getRequestHistory();
|
|
$last = FakeNutgram::getActualData(array_values($history[array_key_last($history)])[0]);
|
|
$this->assertStringContainsString((string) $chatId, $last['text'] ?? '');
|
|
$this->assertStringContainsString('Chat ID', $last['text'] ?? '');
|
|
$this->assertStringContainsString('代理测试群', $last['text'] ?? '');
|
|
$copy = (string) data_get($last, 'reply_markup.inline_keyboard.0.0.copy_text.text', '');
|
|
$this->assertSame((string) $chatId, $copy);
|
|
}
|
|
|
|
#[Test]
|
|
public function promoting_or_leaving_does_not_announce_chat_id(): void
|
|
{
|
|
/** @var FakeNutgram $bot */
|
|
$bot = app(Nutgram::class);
|
|
|
|
$bot->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [
|
|
'chat' => ['id' => -1001, 'type' => ChatType::SUPERGROUP->value, 'title' => 'x'],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'date' => time(),
|
|
'old_chat_member' => [
|
|
'status' => ChatMemberStatus::MEMBER->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
'new_chat_member' => [
|
|
'status' => ChatMemberStatus::ADMINISTRATOR->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
])->reply();
|
|
$bot->assertCalled('sendMessage', 0);
|
|
|
|
$this->app->forgetInstance(Nutgram::class);
|
|
$this->app->forgetInstance('nutgram');
|
|
$this->app->forgetInstance('telegram');
|
|
$this->app->forgetInstance(FakeNutgram::class);
|
|
|
|
/** @var FakeNutgram $bot2 */
|
|
$bot2 = app(Nutgram::class);
|
|
$bot2->hearUpdateType(UpdateType::MY_CHAT_MEMBER, [
|
|
'chat' => ['id' => -1001, 'type' => ChatType::SUPERGROUP->value, 'title' => 'x'],
|
|
'from' => ['id' => self::USER_ID, 'is_bot' => false, 'first_name' => 'Admin'],
|
|
'date' => time(),
|
|
'old_chat_member' => [
|
|
'status' => ChatMemberStatus::MEMBER->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
'new_chat_member' => [
|
|
'status' => ChatMemberStatus::LEFT->value,
|
|
'user' => ['id' => 1, 'is_bot' => true, 'first_name' => 'Bot'],
|
|
],
|
|
])->reply();
|
|
$bot2->assertCalled('sendMessage', 0);
|
|
}
|
|
|
|
/** @param array<int, mixed> $history */
|
|
private function sendMessagePayloads(array $history): array
|
|
{
|
|
$messages = [];
|
|
foreach ($history as $entry) {
|
|
$req = FakeNutgram::getActualData(array_values($entry)[0]);
|
|
if (! isset($req['text'])) {
|
|
continue;
|
|
}
|
|
$messages[] = $req;
|
|
}
|
|
|
|
return $messages;
|
|
}
|
|
}
|