'test-client', 'dingtalk.client_secret' => 'test-secret', 'dingtalk.app_token_url' => 'https://example.test/token', 'dingtalk.timeout' => 3, 'dingtalk.attendance.page_size' => 2, 'dingtalk.attendance.max_pages' => 10, ]); } public function test_attendance_records_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', ]); } $offset = (int) ($request->data()['offset'] ?? 0); if ($offset === 0) { return Http::response([ 'errcode' => 0, 'errmsg' => 'ok', 'recordresult' => [ ['id' => 1, 'userId' => 'user-a', 'workDate' => 1793721600000], ['id' => 2, 'userId' => 'user-a', 'workDate' => 1793721600000], ], ]); } return Http::response([ 'errcode' => 0, 'errmsg' => 'ok', 'recordresult' => [ ['id' => 2, 'userId' => 'user-a', 'workDate' => 1793721600000], ], ]); }); $result = app(DingTalkAttendanceClient::class)->listRecords( ['user-a'], Carbon::parse('2026-11-04 00:00:00', 'Asia/Shanghai'), Carbon::parse('2026-11-04 23:59:59', 'Asia/Shanghai'), ); $this->assertSame(2, $result['pages']); $this->assertSame([1, 2], array_column($result['records'], 'id')); Http::assertSentCount(3); Http::assertSent(static fn (Request $request): bool => str_contains($request->url(), '/attendance/list?access_token=test-token') && ($request->data()['userIdList'] ?? []) === ['user-a'] && ($request->data()['limit'] ?? null) === 2 ); } public function test_missing_attendance_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/attendance/list*' => Http::response([ 'errcode' => 60011, 'errmsg' => 'requiredScopes=[qyapi_attendance_isv_query_result,qyapi_get_attendance_data]', ]), ]); try { app(DingTalkAttendanceClient::class)->listRecords( ['user-a'], Carbon::parse('2026-11-04 00:00:00', 'Asia/Shanghai'), Carbon::parse('2026-11-04 23:59:59', 'Asia/Shanghai'), ); $this->fail('缺少考勤查询权限时应抛出异常。'); } catch (DingTalkAttendanceException $exception) { $this->assertSame('dingtalk_attendance_permission_required', $exception->errorCode); } } }