'test-client', 'dingtalk.client_secret' => 'test-secret', 'dingtalk.app_token_url' => 'https://example.test/token', 'dingtalk.timeout' => 3, 'dingtalk.worklog.template_name' => '财务日报', 'dingtalk.worklog.page_size' => 2, 'dingtalk.worklog.max_pages' => 10, ]); } public function test_reports_are_read_across_pages_and_deduplicated(): void { Http::fake(function (Request $request) { if (str_starts_with($request->url(), 'https://example.test/token')) { return Http::response([ 'errcode' => 0, 'access_token' => 'test-token', 'expires_in' => 7200, ]); } $cursor = (int) ($request->data()['cursor'] ?? 0); if ($cursor === 0) { return Http::response([ 'errcode' => 0, 'result' => [ 'data_list' => [ [ 'report_id' => 'report-a', 'creator_name' => '秋香', 'creator_userid' => 'user-a', 'template_name' => '财务日报', 'create_time' => 1793768760000, ], [ 'report_id' => 'report-b', 'creator_name' => '李斯', 'creator_id' => 'user-b', 'template_name' => '财务日报', 'create_time' => 1793772000000, ], ], 'has_more' => true, 'next_cursor' => 2, ], ]); } return Http::response([ 'errcode' => 0, 'result' => [ 'data_list' => [[ 'report_id' => 'report-b', 'creator_name' => '李斯', 'creator_id' => 'user-b', 'template_name' => '财务日报', 'create_time' => 1793772000000, ]], 'has_more' => false, 'next_cursor' => 0, ], ]); }); $result = app(DingTalkWorklogClient::class)->listReports( Carbon::parse('2026-10-01 00:00:00', 'Asia/Shanghai'), Carbon::parse('2026-11-05 23:59:59', 'Asia/Shanghai'), '财务日报', ); $this->assertSame(2, $result['pages']); $this->assertSame(['report-a', 'report-b'], array_column($result['reports'], 'report_id')); Http::assertSentCount(3); Http::assertSent(static fn (Request $request): bool => str_contains($request->url(), '/topapi/report/list?access_token=test-token') && ($request->data()['template_name'] ?? null) === '财务日报' && ($request->data()['size'] ?? null) === 2 ); } public function test_missing_report_query_permission_has_a_stable_error_code(): void { Http::fake([ 'https://example.test/token*' => Http::response([ 'errcode' => 0, 'access_token' => 'test-token', ]), 'https://oapi.dingtalk.com/topapi/report/list*' => Http::response([ 'errcode' => 88, 'subcode' => 60011, 'errmsg' => 'missing permission: qyapi_report_query', ]), ]); try { app(DingTalkWorklogClient::class)->listReports( Carbon::parse('2026-10-01 00:00:00', 'Asia/Shanghai'), Carbon::parse('2026-11-05 23:59:59', 'Asia/Shanghai'), '财务日报', ); $this->fail('缺少日志查询权限时应抛出异常。'); } catch (DingTalkWorklogException $exception) { $this->assertSame('dingtalk_worklog_permission_required', $exception->errorCode); } } }