516 lines
24 KiB
PHP
516 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Auth;
|
|
|
|
use App\Models\Auth\DingTalkBindingRequest;
|
|
use App\Models\Auth\DingTalkIdentity;
|
|
use App\Models\Auth\DingTalkOAuthState;
|
|
use App\Models\Auth\User;
|
|
use App\Models\Common\Contact;
|
|
use App\Services\Auth\FinanceRoster;
|
|
use App\Utilities\SidebarPermissions;
|
|
use Illuminate\Http\Client\Request as ClientRequest;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class DingTalkLoginTest extends FeatureTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
config([
|
|
'app.url' => 'https://caiwu.jiansuntuzhi.cn:8442',
|
|
'dingtalk.enabled' => true,
|
|
'dingtalk.client_id' => 'test-client-id',
|
|
'dingtalk.client_secret' => 'test-client-secret',
|
|
'dingtalk.redirect_uri' => 'https://caiwu.jiansuntuzhi.cn:8442/auth/dingtalk/callback',
|
|
'dingtalk.authorize_url' => 'https://login.dingtalk.test/oauth2/auth',
|
|
'dingtalk.token_url' => 'https://api.dingtalk.test/v1.0/oauth2/userAccessToken',
|
|
'dingtalk.user_url' => 'https://api.dingtalk.test/v1.0/contact/users/me',
|
|
'dingtalk.app_token_url' => 'https://oapi.dingtalk.test/gettoken',
|
|
'dingtalk.user_by_union_id_url' => 'https://oapi.dingtalk.test/topapi/user/getbyunionid',
|
|
'dingtalk.user_detail_url' => 'https://oapi.dingtalk.test/topapi/v2/user/get',
|
|
]);
|
|
app('url')->forceRootUrl(config('app.url'));
|
|
app('url')->forceScheme('https');
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->andReturn([
|
|
'roster_id' => 'fixture-1', 'display_name' => '财务测试员工', 'department_ids' => ['22848291'],
|
|
])->byDefault();
|
|
}
|
|
|
|
public function test_login_page_shows_dingtalk_entry_without_password_binding_fields(): void
|
|
{
|
|
$this->get(route('login'))
|
|
->assertOk()
|
|
->assertSeeText(trans('auth.dingtalk.login'))
|
|
->assertDontSee('name="dingtalk_binding"', false)
|
|
->assertDontSeeText('首次使用钉钉登录,请输入现有财务系统账号和密码完成绑定');
|
|
}
|
|
|
|
public function test_redirect_creates_hashed_state_and_uses_domain_callback(): void
|
|
{
|
|
$response = $this->get(route('login.dingtalk'))->assertRedirect();
|
|
parse_str((string) parse_url($response->headers->get('Location'), PHP_URL_QUERY), $query);
|
|
|
|
$this->assertSame('test-client-id', $query['client_id']);
|
|
$this->assertSame('openid', $query['scope']);
|
|
$this->assertSame(config('dingtalk.redirect_uri'), $query['redirect_uri']);
|
|
$this->assertDatabaseHas('dingtalk_oauth_states', [
|
|
'state_hash' => hash('sha256', $query['state']),
|
|
'consumed_at' => null,
|
|
]);
|
|
$this->assertDatabaseMissing('dingtalk_oauth_states', ['state_hash' => $query['state']]);
|
|
}
|
|
|
|
public function test_bound_active_enterprise_identity_logs_in_without_storing_access_tokens(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-bound', 'open-bound', email: null));
|
|
|
|
DingTalkIdentity::query()->create([
|
|
'user_id' => $this->user->id,
|
|
'union_id' => 'union-bound',
|
|
'dingtalk_user_id' => 'ding-user-1',
|
|
]);
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'authCode' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirectContains('https://caiwu.jiansuntuzhi.cn:8442');
|
|
|
|
$this->assertAuthenticatedAs($this->user);
|
|
$this->assertDatabaseHas('dingtalk_auth_audits', [
|
|
'event' => 'login_success',
|
|
'outcome' => 'success',
|
|
'user_id' => $this->user->id,
|
|
]);
|
|
$this->assertFalse(
|
|
collect(DingTalkOAuthState::query()->first()->getAttributes())
|
|
->contains(fn ($value) => is_string($value) && str_contains($value, 'access-token'))
|
|
);
|
|
Http::assertSentCount(5);
|
|
}
|
|
|
|
public function test_finance_staff_are_provisioned_without_claiming_an_existing_email_account(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-auto', 'open-auto', email: 'finance@example.test'));
|
|
$this->user->update(['email' => 'finance@example.test']);
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirectContains('https://caiwu.jiansuntuzhi.cn:8442');
|
|
|
|
$created = auth()->user();
|
|
$this->assertNotSame($this->user->id, $created->id);
|
|
$this->assertTrue($created->hasRole('user'));
|
|
$this->assertFalse($created->hasRole('super_admin'));
|
|
$this->assertNull(SidebarPermissions::allowedKeys($created));
|
|
$this->assertTrue(collect(SidebarPermissions::navigationFor($created))->contains('slug', 'store-management'));
|
|
$this->assertDatabaseHas('dingtalk_identities', [
|
|
'user_id' => $created->id,
|
|
'union_id' => 'union-auto',
|
|
'dingtalk_user_id' => 'ding-user-1',
|
|
]);
|
|
$this->assertDatabaseHas('dingtalk_binding_requests', [
|
|
'union_id' => 'union-auto',
|
|
'status' => 'approved',
|
|
'matched_user_id' => $created->id,
|
|
]);
|
|
$this->assertDatabaseHas('dingtalk_auth_audits', [
|
|
'event' => 'account_auto_created',
|
|
'outcome' => 'success',
|
|
'user_id' => $created->id,
|
|
]);
|
|
$this->get(route('dashboard', ['company_id' => $this->company->id]))->assertRedirectContains('/workspace/');
|
|
}
|
|
|
|
public function test_shared_mobile_does_not_claim_an_existing_account(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-mobile', 'open-mobile', email: null, mobile: '+86 138-0013-8000'));
|
|
Contact::query()->create([
|
|
'company_id' => $this->company->id,
|
|
'type' => Contact::EMPLOYEE_TYPE,
|
|
'name' => $this->user->name,
|
|
'email' => $this->user->email,
|
|
'user_id' => $this->user->id,
|
|
'phone' => '13800138000',
|
|
'currency_code' => default_currency(),
|
|
'enabled' => true,
|
|
'created_from' => 'test',
|
|
'created_by' => $this->user->id,
|
|
]);
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirectContains('https://caiwu.jiansuntuzhi.cn:8442');
|
|
|
|
$this->assertNotSame($this->user->id, auth()->id());
|
|
$this->assertDatabaseHas('dingtalk_identities', [
|
|
'user_id' => auth()->id(),
|
|
'union_id' => 'union-mobile',
|
|
]);
|
|
}
|
|
|
|
public function test_finance_staff_receive_api_profile_and_no_manual_registration_step(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses(
|
|
'union-pending',
|
|
'open-pending',
|
|
email: 'outsider@example.test',
|
|
mobile: '13800138000'
|
|
));
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirectContains('https://caiwu.jiansuntuzhi.cn:8442');
|
|
|
|
$this->assertAuthenticated();
|
|
$this->assertDatabaseHas('dingtalk_identities', ['union_id' => 'union-pending', 'user_id' => auth()->id()]);
|
|
$this->assertSame('outsider@example.test', auth()->user()->email);
|
|
$this->assertSame('企业员工', auth()->user()->name);
|
|
$this->assertDatabaseHas('contacts', ['user_id' => auth()->id(), 'phone' => '13800138000']);
|
|
$this->assertDatabaseHas('dingtalk_binding_requests', [
|
|
'union_id' => 'union-pending',
|
|
'status' => 'approved',
|
|
'email_hint' => 'o***@example.test',
|
|
'mobile_hint' => '*******8000',
|
|
]);
|
|
$row = (array) DB::table('dingtalk_binding_requests')->where('union_id', 'union-pending')->first();
|
|
$this->assertStringNotContainsString('outsider@example.test', json_encode($row));
|
|
$this->assertStringNotContainsString('13800138000', json_encode($row));
|
|
}
|
|
|
|
public function test_inactive_enterprise_user_is_denied_without_creating_an_account(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-inactive', 'open-inactive', active: false));
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirect('https://caiwu.jiansuntuzhi.cn:8442/auth/login');
|
|
|
|
$this->assertGuest();
|
|
$this->assertDatabaseMissing('dingtalk_identities', ['union_id' => 'union-inactive']);
|
|
$this->assertDatabaseMissing('dingtalk_binding_requests', ['union_id' => 'union-inactive']);
|
|
$this->assertDatabaseHas('dingtalk_auth_audits', [
|
|
'event' => 'login_denied',
|
|
'outcome' => 'denied',
|
|
]);
|
|
}
|
|
|
|
public function test_external_dingtalk_user_is_denied_when_enterprise_application_cannot_resolve_it(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-external', 'open-external', enterpriseMember: false));
|
|
$userCount = User::query()->count();
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirect('https://caiwu.jiansuntuzhi.cn:8442/auth/login');
|
|
|
|
$this->assertGuest();
|
|
$this->assertSame($userCount, User::query()->count());
|
|
$this->assertDatabaseMissing('dingtalk_binding_requests', ['union_id' => 'union-external']);
|
|
$this->assertDatabaseHas('dingtalk_auth_audits', [
|
|
'event' => 'login_denied',
|
|
'outcome' => 'denied',
|
|
]);
|
|
}
|
|
|
|
public function test_rejected_binding_request_cannot_be_automatically_reactivated(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-rejected', 'open-rejected', email: 'finance@example.test'));
|
|
$this->user->update(['email' => 'finance@example.test']);
|
|
DingTalkBindingRequest::query()->create([
|
|
'union_id' => 'union-rejected',
|
|
'status' => DingTalkBindingRequest::REJECTED,
|
|
'match_reason' => 'administrator_rejected',
|
|
'last_attempted_at' => now()->subMinute(),
|
|
]);
|
|
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $state,
|
|
]))->assertRedirect('https://caiwu.jiansuntuzhi.cn:8442/auth/login');
|
|
|
|
$this->assertGuest();
|
|
$this->assertDatabaseMissing('dingtalk_identities', ['union_id' => 'union-rejected']);
|
|
$this->assertDatabaseHas('dingtalk_binding_requests', [
|
|
'union_id' => 'union-rejected',
|
|
'status' => DingTalkBindingRequest::REJECTED,
|
|
]);
|
|
}
|
|
|
|
public function test_expired_or_replayed_state_never_calls_dingtalk(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
$rawState = 'expired-state';
|
|
DingTalkOAuthState::query()->create([
|
|
'state_hash' => hash('sha256', $rawState),
|
|
'expires_at' => now()->subMinute(),
|
|
]);
|
|
$this->withSession(['dingtalk_oauth_state_hash' => hash('sha256', $rawState)]);
|
|
|
|
$this->get(route('login.dingtalk.callback', [
|
|
'code' => 'authorization-code',
|
|
'state' => $rawState,
|
|
]))->assertRedirect('https://caiwu.jiansuntuzhi.cn:8442/auth/login');
|
|
|
|
Http::assertNothingSent();
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_callback_requires_the_browser_that_started_authorization(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
$state = $this->startAuthorization();
|
|
$this->app['session']->forget('dingtalk_oauth_state_hash');
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertSessionHas('dingtalk_error', trans('auth.dingtalk.failed'));
|
|
Http::assertNothingSent();
|
|
$this->assertGuest();
|
|
$this->assertDatabaseHas('dingtalk_oauth_states', ['state_hash' => hash('sha256', $state), 'consumed_at' => null]);
|
|
}
|
|
|
|
public function test_consumed_state_cannot_be_used_again_with_matching_browser_state(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
$state = $this->startAuthorization();
|
|
DingTalkOAuthState::query()->update(['consumed_at' => now()]);
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertSessionHas('dingtalk_error', trans('auth.dingtalk.failed'));
|
|
Http::assertNothingSent();
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_legacy_callback_returns_to_domain_before_consuming_state(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
$state = $this->startAuthorization();
|
|
$response = $this->get('http://218.93.190.75:8442/auth/dingtalk/callback?'.http_build_query([
|
|
'code' => 'fake-code', 'state' => $state, 'redirect' => 'https://untrusted.test',
|
|
]))->assertRedirect();
|
|
$this->assertStringStartsWith(config('app.url').'/auth/dingtalk/callback?', $response->headers->get('Location'));
|
|
$this->assertStringNotContainsString('untrusted', $response->headers->get('Location'));
|
|
$response->assertHeader('Referrer-Policy', 'no-referrer');
|
|
$this->assertDatabaseHas('dingtalk_oauth_states', ['state_hash' => hash('sha256', $state), 'consumed_at' => null]);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_revoked_binding_invalidates_logged_in_session_on_next_request(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-session', 'open-session', email: null));
|
|
$identity = DingTalkIdentity::query()->create(['user_id' => $this->user->id, 'union_id' => 'union-session']);
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertSessionHas('dingtalk_identity_id', $identity->id);
|
|
$identity->delete();
|
|
$this->getJson(route('dashboard', ['company_id' => $this->company->id]))->assertUnauthorized();
|
|
$this->assertGuest();
|
|
$this->assertFalse(session()->has('dingtalk_identity_id'));
|
|
}
|
|
|
|
public function test_non_finance_staff_cannot_create_accounts_even_with_a_matching_email(): void
|
|
{
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->andReturnNull();
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-outside', 'open-outside', email: $this->user->email));
|
|
$count = User::count();
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertSessionHas('dingtalk_error', trans('auth.dingtalk.outside_roster'));
|
|
$this->assertGuest();
|
|
$this->assertSame($count, User::count());
|
|
}
|
|
|
|
public function test_roster_outage_does_not_create_or_authorize_an_account(): void
|
|
{
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->andThrow(new \RuntimeException('fixture outage'));
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-outage', 'open-outage'));
|
|
$count = User::count();
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertSessionHas('dingtalk_error', trans('auth.dingtalk.roster_unavailable'));
|
|
$this->assertGuest();
|
|
$this->assertSame($count, User::count());
|
|
}
|
|
|
|
public function test_repeat_authorization_keeps_the_same_account_and_does_not_remember_login(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-repeat', 'open-repeat'));
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]))
|
|
->assertCookieExpired(auth()->guard()->getRecallerName());
|
|
$id = auth()->id();
|
|
$count = User::count();
|
|
auth()->logout();
|
|
session()->invalidate();
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'another-code', 'state' => $state]));
|
|
$this->assertSame($id, auth()->id());
|
|
$this->assertSame($count, User::count());
|
|
$this->assertSame(1, DingTalkIdentity::where('union_id', 'union-repeat')->count());
|
|
}
|
|
|
|
public function test_roster_removal_invalidates_session_at_recheck(): void
|
|
{
|
|
$identity = DingTalkIdentity::create(['user_id' => $this->user->id, 'union_id' => 'union-removed', 'dingtalk_user_id' => 'ding-removed']);
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->with('ding-removed')->andReturnNull();
|
|
$this->actingAs($this->user)->withSession([
|
|
'dingtalk_identity_id' => $identity->id,
|
|
'dingtalk_roster_checked_at' => now()->subMinutes(6)->timestamp,
|
|
])->getJson(route('dashboard', ['company_id' => $this->company->id]))->assertUnauthorized();
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_disabled_account_cannot_keep_using_dingtalk_session(): void
|
|
{
|
|
$identity = DingTalkIdentity::create(['user_id' => $this->user->id, 'union_id' => 'union-disabled']);
|
|
$this->user->update(['enabled' => false]);
|
|
$this->actingAs($this->user)->withSession([
|
|
'dingtalk_identity_id' => $identity->id,
|
|
'dingtalk_roster_checked_at' => now()->timestamp,
|
|
])->getJson(route('dashboard', ['company_id' => $this->company->id]))->assertUnauthorized();
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_bound_disabled_account_is_not_recreated(): void
|
|
{
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-disabled-login', 'open-disabled'));
|
|
DingTalkIdentity::create(['user_id' => $this->user->id, 'union_id' => 'union-disabled-login']);
|
|
$this->user->update(['enabled' => false]);
|
|
$count = User::count();
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]));
|
|
$this->assertGuest();
|
|
$this->assertSame($count, User::count());
|
|
}
|
|
|
|
public function test_password_session_for_provisioned_user_cannot_bypass_roster(): void
|
|
{
|
|
$this->user->update(['created_from' => 'dingtalk_roster']);
|
|
DingTalkIdentity::create(['user_id' => $this->user->id, 'union_id' => 'union-password', 'dingtalk_user_id' => 'ding-password']);
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->with('ding-password')->andReturnNull();
|
|
$this->actingAs($this->user)->getJson(route('dashboard', ['company_id' => $this->company->id]))->assertUnauthorized();
|
|
$this->assertGuest();
|
|
}
|
|
|
|
public function test_roster_outage_during_session_recheck_serves_no_business_data(): void
|
|
{
|
|
$identity = DingTalkIdentity::create(['user_id' => $this->user->id, 'union_id' => 'union-recheck', 'dingtalk_user_id' => 'ding-recheck']);
|
|
$this->mock(FinanceRoster::class)->shouldReceive('findEligible')->andThrow(new \RuntimeException('fixture outage'));
|
|
$this->actingAs($this->user)->withSession(['dingtalk_identity_id' => $identity->id])
|
|
->getJson(route('dashboard', ['company_id' => $this->company->id]))->assertStatus(503);
|
|
$this->assertAuthenticated();
|
|
$this->assertFalse(session()->has('dingtalk_roster_checked_at'));
|
|
}
|
|
|
|
public function test_admin_can_assign_business_access_to_the_automatically_created_user(): void
|
|
{
|
|
app('url')->defaults(['company_id' => $this->company->id]);
|
|
Http::preventStrayRequests();
|
|
Http::fake($this->dingtalkResponses('union-grant', 'open-grant'));
|
|
$state = $this->startAuthorization();
|
|
$this->get(route('login.dingtalk.callback', ['code' => 'fake-code', 'state' => $state]));
|
|
$created = auth()->user();
|
|
$this->assertNull(SidebarPermissions::allowedKeys($created));
|
|
auth()->logout(); session()->invalidate();
|
|
$this->actingAs($this->user)->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$created->id]))
|
|
->assertOk()->assertSee('value="user_'.$created->id.'"', false);
|
|
$this->patch(route('system-settings.sidebar.update'), [
|
|
'subject' => 'user_'.$created->id, 'permissions' => ['store-management/basic-information'],
|
|
])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$created->id]));
|
|
$this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($created));
|
|
$this->actingAs($created)->withSession(['dingtalk_identity_id' => $created->dingtalkIdentity->id, 'dingtalk_roster_checked_at' => now()->timestamp])
|
|
->get(route('dashboard'))->assertRedirectContains('/workspace/store-management/basic-information');
|
|
$this->withExceptionHandling()->get(route('business.pages.show', [
|
|
'company_id' => $this->company->id, 'section' => 'company-management', 'page' => 'basic-information',
|
|
]))->assertForbidden();
|
|
}
|
|
|
|
public function test_explicit_empty_user_scope_does_not_fall_back_to_all_pages(): void
|
|
{
|
|
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
|
setting()->set('system.sidebar_permissions', [
|
|
'configured' => ['user_'.$this->user->id], 'subjects' => ['user_'.$this->user->id => []],
|
|
]);
|
|
$this->assertSame([], SidebarPermissions::navigationFor($this->user));
|
|
$this->loginAs()->get(route('dashboard', ['company_id' => $this->company->id]))->assertOk()->assertSeeText('尚未分配业务权限');
|
|
}
|
|
|
|
protected function startAuthorization(): string
|
|
{
|
|
$response = $this->get(route('login.dingtalk'))->assertRedirect();
|
|
parse_str((string) parse_url($response->headers->get('Location'), PHP_URL_QUERY), $query);
|
|
|
|
return $query['state'];
|
|
}
|
|
|
|
protected function dingtalkResponses(
|
|
string $unionId,
|
|
string $openId,
|
|
?string $email = 'finance@example.test',
|
|
?string $mobile = null,
|
|
bool $active = true,
|
|
bool $enterpriseMember = true
|
|
): callable {
|
|
return function (ClientRequest $request) use ($unionId, $openId, $email, $mobile, $active, $enterpriseMember) {
|
|
$url = $request->url();
|
|
|
|
return match (true) {
|
|
$url === 'https://api.dingtalk.test/v1.0/oauth2/userAccessToken' => Http::response([
|
|
'accessToken' => 'access-token-not-persisted',
|
|
'expireIn' => 7200,
|
|
]),
|
|
$url === 'https://api.dingtalk.test/v1.0/contact/users/me' => Http::response([
|
|
'unionId' => $unionId,
|
|
'openId' => $openId,
|
|
'nick' => '企业员工',
|
|
]),
|
|
Str::startsWith($url, 'https://oapi.dingtalk.test/gettoken?') => Http::response([
|
|
'errcode' => 0,
|
|
'access_token' => 'app-access-token-not-persisted',
|
|
]),
|
|
Str::startsWith($url, 'https://oapi.dingtalk.test/topapi/user/getbyunionid?') => Http::response([
|
|
'errcode' => $enterpriseMember ? 0 : 60121,
|
|
'result' => $enterpriseMember ? ['userid' => 'ding-user-1'] : null,
|
|
]),
|
|
Str::startsWith($url, 'https://oapi.dingtalk.test/topapi/v2/user/get?') => Http::response([
|
|
'errcode' => 0,
|
|
'result' => [
|
|
'userid' => 'ding-user-1',
|
|
'name' => '企业员工',
|
|
'email' => $email,
|
|
'mobile' => $mobile,
|
|
'active' => $active,
|
|
],
|
|
]),
|
|
default => Http::response([], 404),
|
|
};
|
|
};
|
|
}
|
|
}
|