diff --git a/.gitignore b/.gitignore index 2b6416e..46e2ab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,48 @@ +# custom li2u-output .vscode +geckodriver.log + +# MacOS +.DS_Store + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ \ No newline at end of file diff --git a/linkedin2username.py b/linkedin2username.py index 9158e5f..efa7560 100755 --- a/linkedin2username.py +++ b/linkedin2username.py @@ -35,25 +35,57 @@ """ -# The dictionary below is a best-effort attempt to spread a search load -# across sets of geographic locations. This can bypass the 1000 result -# search limit as we are now allowed 1000 per geo set. -# developer.linkedin.com/docs/v1/companies/targeting-company-shares#additionalcodes +# The dictionary below contains geo region codes. Because we are limited to 1000 results per search, +# we can use this to batch searches across regions and get more results. +# I found this in some random JS, so who knows if it will change. +# https://static.licdn.com/aero-v1/sc/h/6pw526ylxpzsa7nu7ht18bo8y GEO_REGIONS = { - 'r0': 'us:0', - 'r1': 'ca:0', - 'r2': 'gb:0', - 'r3': 'au:0|nz:0', - 'r4': 'cn:0|hk:0', - 'r5': 'jp:0|kr:0|my:0|np:0|ph:0|sg:0|lk:0|tw:0|th:0|vn:0', - 'r6': 'in:0', - 'r7': 'at:0|be:0|bg:0|hr:0|cz:0|dk:0|fi:0', - 'r8': 'fr:0|de:0', - 'r9': 'gr:0|hu:0|ie:0|it:0|lt:0|nl:0|no:0|pl:0|pt:0', - 'r10': 'ro:0|ru:0|rs:0|sk:0|es:0|se:0|ch:0|tr:0|ua:0', - 'r11': ('ar:0|bo:0|br:0|cl:0|co:0|cr:0|do:0|ec:0|gt:0|mx:0|pa:0|pe:0' - '|pr:0|tt:0|uy:0|ve:0'), - 'r12': 'af:0|bh:0|il:0|jo:0|kw:0|pk:0|qa:0|sa:0|ae:0'} + "ar": "100446943", + "at": "103883259", + "au": "101452733", + "be": "100565514", + "bg": "105333783", + "ca": "101174742", + "ch": "106693272", + "cl": "104621616", + "de": "101282230", + "dk": "104514075", + "es": "105646813", + "fi": "100456013", + "fo": "104630756", + "fr": "105015875", + "gb": "101165590", + "gf": "105001561", + "gp": "104232339", + "gr": "104677530", + "gu": "107006862", + "hr": "104688944", + "hu": "100288700", + "is": "105238872", + "it": "103350119", + "li": "100878084", + "lu": "104042105", + "mq": "103091690", + "nl": "102890719", + "no": "103819153", + "nz": "105490917", + "pe": "102927786", + "pl": "105072130", + "pr": "105245958", + "pt": "100364837", + "py": "104065273", + "re": "104265812", + "rs": "101855366", + "ru": "101728296", + "se": "105117694", + "sg": "102454443", + "si": "106137034", + "tw": "104187078", + "ua": "102264497", + "us": "103644278", + "uy": "100867946", + "ve": "101490751" +} class NameMutator(): @@ -205,7 +237,7 @@ def parse_arguments(): ) parser.add_argument('-d', '--depth', type=int, action='store', default=False, - help='Search depth (how many loops of 25). If unset, ' + help='Search depth (how many loops of 50). If unset, ' 'will try to grab them all.') parser.add_argument('-s', '--sleep', type=int, action='store', default=0, help='Seconds to sleep between search loops.' @@ -405,9 +437,9 @@ def set_inner_loops(staff_count, args): """ - # We will look for 25 names on each loop. So, we set a maximum amount of - # loops to the amount of staff / 25 +1 more to catch remainders. - loops = int((staff_count / 25) + 1) + # We will look for 50 names on each loop. So, we set a maximum amount of + # loops to the amount of staff / 50 +1 more to catch remainders. + loops = int((staff_count / 50) + 1) print(f"[*] Company has {staff_count} profiles to check. Some may be anonymous.") @@ -435,7 +467,7 @@ def set_inner_loops(staff_count, args): " might not get them all.\n\n") else: print(f"[*] Setting each iteration to a maximum of {loops} loops of" - " 25 results each.\n\n") + " 50 results each.\n\n") args.depth = loops return args.depth, args.geoblast @@ -448,25 +480,23 @@ def get_results(session, company_id, page, region, keyword): scrolling through search results. The mobile site defaults to using a 'count' of 10, but testing shows that - 25 is allowed. This behavior will appear to the web server as someone + 50 is allowed. This behavior will appear to the web server as someone scrolling quickly through all available results. """ - # When using the --geoblast feature, we need to inject our set of region - # codes into the search parameter. - if region: - region = re.sub(':', '%3A', region) # must URL encode this parameter # Build the base search URL. - url = ('https://www.linkedin.com' - '/voyager/api/search/hits' - f'?facetCurrentCompany=List({company_id})' - f'&facetGeoRegion=List({region})' - f'&keywords=List({keyword})' - '&q=people&maxFacetValues=15' - '&supportedFacets=List(GEO_REGION,CURRENT_COMPANY)' - '&count=25' - '&origin=organization' - f'&start={page * 25}') + url = ('https://www.linkedin.com/voyager/api/graphql?variables=(' + f'start:{page * 50},' + f'query:(' + f'{f"keywords:{keyword}," if keyword else ""}' + 'flagshipSearchIntent:SEARCH_SRP,' + f'queryParameters:List((key:currentCompany,value:List({company_id})),' + f'{f"(key:geoUrn,value:List({region}))," if region else ""}' + '(key:resultType,value:List(PEOPLE))' + '),' + 'includeFiltersInResponse:false' + '),count:50)' + '&queryId=voyagerSearchDashClusters.66adc6056cf4138949ca5dcb31bb1749') # Perform the search for this iteration. result = session.get(url) @@ -475,9 +505,9 @@ def get_results(session, company_id, page, region, keyword): def find_employees(result): """ - Takes the text response of an HTTP query, converst to JSON, and extracts employee details. + Takes the text response of an HTTP query, converts to JSON, and extracts employee details. - Retuns a list of dictionary items, or False if none found. + Returns a list of dictionary items, or False if none found. """ found_employees = [] @@ -491,33 +521,43 @@ def find_employees(result): print(result[:200]) return False - # When you get to the last page of results, the next page will have an empty - # "elements" list. - if not result_json['elements']: + # Walk the data, being careful to avoid key errors + data = result_json.get('data', {}) + search_clusters = data.get('searchDashClustersByAll', {}) + elements = paging = search_clusters.get('elements', []) + paging = search_clusters.get('paging', {}) + total = paging.get('total', 0) + + # If we've ended up with empty dicts or zero results left, bail out + if total == 0: return False # The "elements" list is the mini-profile you see when scrolling through a # company's employees. It does not have all info on the person, like their # entire job history. It only has some basics. found_employees = [] - for body in result_json.get('elements', []): - profile = ( - body.get('hitInfo', {}) - .get('com.linkedin.voyager.search.SearchProfile', {}) - .get('miniProfile', {}) - ) - first_name = profile.get('firstName', '').strip() - last_name = profile.get('lastName', '').strip() - - # Dont include profiles that have only a single name - if first_name and last_name: - full_name = f"{first_name} {last_name}" - occupation = profile.get('occupation', "") - found_employees.append({'full_name': full_name, 'occupation': occupation}) + for element in elements: + # For some reason it's nested + for item_body in element.get('items', []): + # Info we want is all under 'entityResult' + entity = item_body['item']['entityResult'] - return found_employees + # There's some useless entries we need to skip over + if not entity: + continue + + # There is no first/last name fields anymore so we're taking the full name + full_name = entity['title']['text'].strip() + # The name may include extras like "Dr" at the start, so we do some basic stripping + if full_name[:3] == 'Dr ': + full_name = full_name[4:] + occupation = entity['primarySubtitle']['text'] + + found_employees.append({'full_name': full_name, 'occupation': occupation}) + + return found_employees def do_loops(session, company_id, outer_loops, args): @@ -544,10 +584,10 @@ def do_loops(session, company_id, outer_loops, args): try: for current_loop in outer_loops: if args.geoblast: - region_name = 'r' + str(current_loop) - current_region = GEO_REGIONS[region_name] + region_name, region_id = list(GEO_REGIONS.items())[current_loop] + current_region = region_id current_keyword = '' - print(f"\n[*] Looping through region {current_region}") + print(f"\n[*] Looping through region {region_name}") elif args.keywords: current_keyword = args.keywords[current_loop] current_region = '' @@ -556,7 +596,7 @@ def do_loops(session, company_id, outer_loops, args): current_region = '' current_keyword = '' - # This is the inner loop. It will search results 25 at a time. + # This is the inner loop. It will search results 50 at a time. for page in range(0, args.depth): new_names = 0 diff --git a/tests/mock-employee-response b/tests/mock-employee-response index ffdc182..e494c6e 100644 --- a/tests/mock-employee-response +++ b/tests/mock-employee-response @@ -1 +1 @@ -{"metadata":{"totalResultCount":89966,"searchTieIn":"FREE_UPSELL","origin":"organization","id":"xxxxxx","type":"PEOPLE","facets":[{"facetParameterName":"facetGeoRegion","premiumFacet":false,"facetType":"GEO_REGION","facetTypeV2":{"com.linkedin.voyager.search.PeopleSearchFacetType":"GEO_REGION"},"displayName":"Locations","facetValues":[{"displayValue":"United States","count":29368,"value":"us:0","selected":false},{"displayValue":"Brazil","count":19995,"value":"br:0","selected":false},{"displayValue":"India","count":6883,"value":"in:0","selected":false},{"displayValue":"San Francisco Bay Area","count":5126,"value":"us:84","selected":false},{"displayValue":"São Paulo Area, Brazil","count":4338,"value":"br:6368","selected":false},{"displayValue":"Mexico","count":3922,"value":"mx:0","selected":false},{"displayValue":"Greater New York City Area","count":3152,"value":"us:70","selected":false},{"displayValue":"United Kingdom","count":2464,"value":"gb:0","selected":false},{"displayValue":"Canada","count":1839,"value":"ca:0","selected":false},{"displayValue":"Greater Chicago Area","count":1833,"value":"us:14","selected":false},{"displayValue":"Netherlands","count":1314,"value":"nl:0","selected":false},{"displayValue":"Greater Seattle Area","count":1147,"value":"us:91","selected":false},{"displayValue":"Hyderabad Area, India","count":1083,"value":"in:6508","selected":false},{"displayValue":"Amsterdam Area, Netherlands","count":946,"value":"nl:5664","selected":false},{"displayValue":"Bengaluru Area, India","count":784,"value":"in:7127","selected":false}]},{"facetParameterName":"facetCurrentCompany","premiumFacet":false,"facetType":"CURRENT_COMPANY","facetTypeV2":{"com.linkedin.voyager.search.PeopleSearchFacetType":"CURRENT_COMPANY"},"displayName":"Current companies","facetValues":[{"displayValue":"xxx","count":89966,"value":"xxx","selected":true},{"displayValue":"xxx","count":720315,"value":"xxx","selected":false},{"displayValue":"xxx","count":603730,"value":"xxx","selected":false},{"displayValue":"xxx","count":481637,"value":"xxx","selected":false},{"displayValue":"xxx","count":420387,"value":"xxx","selected":false},{"displayValue":"xxx","count":349971,"value":"xxx","selected":false},{"displayValue":"xxx","count":259645,"value":"xxx","selected":false},{"displayValue":"xxx","count":191950,"value":"xxx","selected":false},{"displayValue":"xxx","count":181587,"value":"xxx","selected":false},{"displayValue":"xxx","count":141447,"value":"xxx","selected":false},{"displayValue":"xxx","count":127794,"value":"xxx","selected":false},{"displayValue":"xxx","count":61962,"value":"xxx","selected":false},{"displayValue":"xxx","count":23018,"value":"xxx","selected":false},{"displayValue":"xxx","count":7012,"value":"xxx","selected":false},{"displayValue":"xxx","count":1802,"value":"xxx","selected":false}]}]},"elements":[{"targetPageInstance":"urn:li:page:d_flagship3_profile_view_base;xxx/VA==","hitInfo":{"com.linkedin.voyager.search.SearchProfile":{"headless":false,"memberBadges":{"premium":false,"influencer":false,"openLink":false,"entityUrn":"urn:li:fs_memberBadges:xxx-xxx","jobSeeker":false},"distance":{"value":"DISTANCE_2"},"snippets":[{"heading":{"annotations":[{"start":27,"end":31,"attribute":{"com.linkedin.voyager.relationships.shared.annotation.Format":{"type":"BOLD"}}}],"text":"xxx at SomeCompany"},"body":{"annotations":[],"text":""},"fieldType":"CURRENT_POSITION"}],"educations":[{"endedOn":{"year":2020},"educationId":672172131,"school":"urn:li:fs_normalized_school:xxx","degree":"xxx","schoolName":"xxx","fieldOfStudy":"xxx","startedOn":{"year":2018}},{"endedOn":{"year":2017},"educationId":123,"school":"urn:li:fs_normalized_school:xxx","degree":"xxx","schoolName":"xxx","fieldOfStudy":"xxx","startedOn":{"year":2013}}],"industry":"Financial Services","miniProfile":{"firstName":"Michael","lastName":"Myers","dashEntityUrn":"urn:li:fsd_profile:xxx-xxx","occupation":"Camp Counsellor","objectUrn":"urn:li:member:xxx","entityUrn":"urn:li:fs_miniProfile:xxx-xxx","publicIdentifier":"mmyershalloween","picture":{"com.linkedin.common.VectorImage":{"artifacts":[{"width":100,"fileIdentifyingUrlPathSegment":"100_100/0/1582370830599?e=1680134400&v=beta&t=xxx","expiresAt":1680134400000,"height":100},{"width":200,"fileIdentifyingUrlPathSegment":"200_200/0/1582370830599?e=1680134400&v=beta&t=xxx","expiresAt":1680134400000,"height":200},{"width":400,"fileIdentifyingUrlPathSegment":"400_400/0/1582370830599?e=1680134400&v=beta&t=B5L7mvEa9_FPhTynFNqaljiIKMKWtUMdEzQLlcGrTU0","expiresAt":1680134400000,"height":400},{"width":800,"fileIdentifyingUrlPathSegment":"xxx","expiresAt":1680134400000,"height":800}],"rootUrl":"https://media.licdn.com/dms/image/xxx/profile-displayphoto-shrink_"}},"trackingId":"xxx/w=="},"id":"xxx-xxx","backendUrn":"urn:li:member:xxx","nameMatch":false}},"trackingId":"xxx/w=="},{"targetPageInstance":"urn:li:page:d_flagship3_profile_view_base;xxx==","hitInfo":{"com.linkedin.voyager.search.SearchProfile":{"headless":false,"memberBadges":{"premium":true,"influencer":false,"openLink":false,"entityUrn":"urn:li:fs_memberBadges:xxx","jobSeeker":false},"distance":{"value":"DISTANCE_2"},"snippets":[{"heading":{"annotations":[{"start":18,"end":22,"attribute":{"com.linkedin.voyager.relationships.shared.annotation.Format":{"type":"BOLD"}}}],"text":"Babysitter"},"body":{"annotations":[],"text":""},"fieldType":"CURRENT_POSITION"}],"educations":[{"endedOn":{"year":2011},"educationId":79973941,"school":"urn:li:fs_normalized_school:xxx","degree":"xxx","schoolName":"xxx","fieldOfStudy":"xxx","startedOn":{"year":2007}}],"industry":"xxx","miniProfile":{"firstName":"Freddy","lastName":"Krueger","dashEntityUrn":"urn:li:fsd_profile:xxx","occupation":"Babysitter","objectUrn":"urn:li:member:137397672","entityUrn":"urn:li:fs_miniProfile:xxx","backgroundImage":{"com.linkedin.common.VectorImage":{"artifacts":[{"width":800,"fileIdentifyingUrlPathSegment":"200_800/0/xxx?e=1680134400&v=beta&t=xxx","expiresAt":1680134400000,"height":200},{"width":1400,"fileIdentifyingUrlPathSegment":"350_1400/0/xxx?e=1680134400&v=beta&t=xxx-mg","expiresAt":1680134400000,"height":350}],"rootUrl":"https://media.licdn.com/dms/image/xxx/profile-displaybackgroundimage-shrink_"}},"publicIdentifier":"thedreammaster","picture":{"com.linkedin.common.VectorImage":{"artifacts":[{"width":100,"fileIdentifyingUrlPathSegment":"100_100/0/xxx?e=1680134400&v=beta&t=xxx-w8D39bMCdDMI","expiresAt":1680134400000,"height":100},{"width":200,"fileIdentifyingUrlPathSegment":"200_200/0/xxx?e=1680134400&v=beta&t=xxx","expiresAt":1680134400000,"height":200},{"width":400,"fileIdentifyingUrlPathSegment":"400_400/0/1516835824309?e=1680134400&v=beta&t=xxx","expiresAt":1680134400000,"height":400},{"width":800,"fileIdentifyingUrlPathSegment":"800_800/0/1516835824309?e=1680134400&v=beta&t=xxx-xxx","expiresAt":1680134400000,"height":800}],"rootUrl":"https://media.licdn.com/dms/image/xxx-Ak-xQ/profile-displayphoto-shrink_"}},"trackingId":"NcaINtk/SD+N6Blr061WPw=="},"id":"ACoAAAgwhagBidzWjblFvWPkqm14v63NTCk5N4U","backendUrn":"urn:li:member:137397672","nameMatch":false}},"trackingId":"NcaINtk/SD+N6Blr061WPw=="}],"paging":{"count":25,"start":0,"total":1000,"links":[]}} \ No newline at end of file +{"data":{"_recipeType":"xxxxx","_type":"xxxxx","searchDashClustersByAll":{"_type":"xxxxx","metadata":{"entityResultAttributes":null,"totalResultCount":666,"searchAlert":null,"_type":"xxxxx","secondaryFilterCluster":null,"_recipeType":"xxxxx","lazyRightRail":null,"queryType":null,"primaryResultType":"xxxxx","paginationToken":null,"primaryFilterCluster":null,"blockedQuery":false,"entityActionButtonStyle":null,"searchId":"xxxxx","filterAppliedCount":0,"clusterTitleFontSize":null,"simpleInsightAttributes":null,"knowledgeCardRightRail":null},"paging":{"count":50,"start":100,"_type":"xxxxx","total":666,"_recipeType":"xxxxx"},"_recipeType":"xxxxx","elements":[{"image":null,"quickFilterActions":[],"clusterRenderType":"xxxxx","dismissable":false,"totalResultCount":null,"_type":"xxxxx","controlName":null,"description":null,"_recipeType":"xxxxx","title":null,"actionTypeName":null,"navigationText":null,"feature":null,"navigationCardAction":null,"position":851,"items":[{"_type":"xxxxx","item":{"entityResult":{"template":"xxxxx","actorNavigationContext":null,"bserpEntityNavigationalUrl":null,"trackingUrn":"xxxxx","controlName":null,"interstitialComponent":null,"primaryActions":[],"entityCustomTrackingInfo":{"memberDistance":"xxxxx","_type":"xxxxx","privacySettingsInjectionHolder":null,"_recipeType":"xxxxx","nameMatch":false},"title":{"textDirection":"xxxxx","_type":"xxxxx","text":"Michael Myers","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":"xxxxx"},"overflowActions":[],"searchActionType":null,"actorInsights":[],"insightsResolutionResults":[],"badgeIcon":null,"entityUrn":"xxxxx","showAdditionalCluster":false,"ringStatus":null,"primarySubtitle":{"textDirection":"xxxxx","_type":"xxxxx","text":"Camp Counsellor","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":null},"badgeText":{"textDirection":"xxxxx","_type":"xxxxx","text":"xxxxx","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":"xxxxx"},"trackingId":"xxxxx","addEntityToSearchHistory":true,"actorNavigationUrl":null,"summary":null,"image":{"_type":"xxxxx","attributes":[{"scalingType":null,"_type":"xxxxx","detailData":{"profilePictureWithoutFrame":null,"profilePictureWithRingStatus":null,"companyLogo":null,"icon":null,"systemImage":null,"nonEntityGroupLogo":null,"vectorImage":null,"nonEntityProfessionalEventLogo":null,"profilePicture":null,"imageUrl":null,"professionalEventLogo":null,"nonEntityCompanyLogo":null,"nonEntitySchoolLogo":null,"groupLogo":null,"schoolLogo":null,"ghostImage":null,"nonEntityProfilePicture":{"_type":"xxxxx","ringStatus":null,"_recipeType":"xxxxx","vectorImage":{"_type":"xxxxx","attribution":null,"_recipeType":"xxxxx","digitalmediaAsset":null,"artifacts":[{"width":100,"_type":"xxxxx","_recipeType":"xxxxx","fileIdentifyingUrlPathSegment":"xxxxx","expiresAt":1702512000000,"height":100}],"rootUrl":"xxxxx"},"profile":{"_recipeType":"xxxxx","_type":"xxxxx","entityUrn":"xxxxx"}}},"tintColor":null,"_recipeType":"xxxxx","tapTargets":[],"displayAspectRatio":null}],"editableAccessibilityText":false,"actionTarget":null,"_recipeType":"xxxxx","accessibilityTextAttributes":[],"totalCount":null,"accessibilityText":"xxxxx"},"lazyLoadedActions":{"_recipeType":"xxxxx","_type":"xxxxx","entityUrn":"xxxxx"},"secondarySubtitle":{"textDirection":"xxxxx","_type":"xxxxx","text":"xxxxx","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":null},"_type":"xxxxx","navigationUrl":"xxxxx","entityEmbeddedObject":null,"unreadIndicatorDetails":null,"_recipeType":"xxxxx","target":null,"actorTrackingUrn":null,"navigationContext":{"_type":"xxxxx","openExternally":false,"_recipeType":"xxxxx","url":"xxxxx"}},"cluster":null,"bannerCard":null,"searchSuggestionCard":null,"feedbackCard":null,"knowledgeCardV2":null,"keywordsSuggestionCard":null,"queryClarificationCard":null,"simpleText":null,"topicalQuestionCard":null,"simpleTextV2":null,"promoCard":null,"centeredText":null,"simpleImage":null},"position":1,"_recipeType":"xxxxx"},{"_type":"xxxxx","item":{"entityResult":{"template":"xxxxx","actorNavigationContext":null,"bserpEntityNavigationalUrl":null,"trackingUrn":"xxxxx","controlName":null,"interstitialComponent":null,"primaryActions":[],"entityCustomTrackingInfo":{"memberDistance":"xxxxx","_type":"xxxxx","privacySettingsInjectionHolder":null,"_recipeType":"xxxxx","nameMatch":false},"title":{"textDirection":"xxxxx","_type":"xxxxx","text":"Freddy Krueger","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":"xxxxx"},"overflowActions":[],"searchActionType":null,"actorInsights":[],"insightsResolutionResults":[{"jobPostingInsight":null,"relationshipsInsight":null,"serviceProviderRatingInsight":null,"simpleInsight":{"searchActionType":"xxxxx","image":{"_type":"xxxxx","attributes":[{"scalingType":null,"_type":"xxxxx","detailData":{"profilePictureWithoutFrame":null,"profilePictureWithRingStatus":null,"companyLogo":null,"icon":null,"systemImage":null,"nonEntityGroupLogo":null,"vectorImage":null,"nonEntityProfessionalEventLogo":null,"profilePicture":null,"imageUrl":null,"professionalEventLogo":null,"nonEntityCompanyLogo":null,"nonEntitySchoolLogo":null,"groupLogo":null,"schoolLogo":null,"ghostImage":null,"nonEntityProfilePicture":{"_type":"xxxxx","ringStatus":null,"_recipeType":"xxxxx","vectorImage":{"_type":"xxxxx","attribution":null,"_recipeType":"xxxxx","digitalmediaAsset":null,"artifacts":[{"width":100,"_type":"xxxxx","_recipeType":"xxxxx","fileIdentifyingUrlPathSegment":"xxxxx","expiresAt":1702512000000,"height":100}],"rootUrl":"xxxxx"},"profile":{"_recipeType":"xxxxx","_type":"xxxxx","entityUrn":"xxxxx"}}},"tintColor":null,"_recipeType":"xxxxx","tapTargets":[],"displayAspectRatio":null}],"editableAccessibilityText":false,"actionTarget":null,"_recipeType":"xxxxx","accessibilityTextAttributes":[],"totalCount":null,"accessibilityText":null},"subtitleMaxNumLines":1,"titleFontSize":null,"subtitle":null,"_type":"xxxxx","controlName":null,"subtitleFontSize":null,"navigationUrl":"xxxxx","title":{"textDirection":"xxxxx","_type":"xxxxx","text":"xxxxx","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":null},"_recipeType":"xxxxx","titleMaxNumLines":1},"jobPostingFooterInsight":null,"socialActivityCountsInsight":null,"labelsInsight":null,"premiumCustomCtaInsight":null}],"badgeIcon":null,"entityUrn":"xxxxx","showAdditionalCluster":false,"ringStatus":null,"primarySubtitle":{"textDirection":"xxxxx","_type":"xxxxx","text":"Babysitter","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":null},"badgeText":{"textDirection":"xxxxx","_type":"xxxxx","text":"xxxxx","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":"xxxxx"},"trackingId":"xxxxx","addEntityToSearchHistory":true,"actorNavigationUrl":null,"summary":null,"image":{"_type":"xxxxx","attributes":[{"scalingType":null,"_type":"xxxxx","detailData":{"profilePictureWithoutFrame":null,"profilePictureWithRingStatus":null,"companyLogo":null,"icon":null,"systemImage":null,"nonEntityGroupLogo":null,"vectorImage":null,"nonEntityProfessionalEventLogo":null,"profilePicture":null,"imageUrl":null,"professionalEventLogo":null,"nonEntityCompanyLogo":null,"nonEntitySchoolLogo":null,"groupLogo":null,"schoolLogo":null,"ghostImage":null,"nonEntityProfilePicture":{"_type":"xxxxx","ringStatus":null,"_recipeType":"xxxxx","vectorImage":{"_type":"xxxxx","attribution":null,"_recipeType":"xxxxx","digitalmediaAsset":null,"artifacts":[{"width":100,"_type":"xxxxx","_recipeType":"xxxxx","fileIdentifyingUrlPathSegment":"xxxxx","expiresAt":1702512000000,"height":100}],"rootUrl":"xxxxx"},"profile":{"_recipeType":"xxxxx","_type":"xxxxx","entityUrn":"xxxxx"}}},"tintColor":null,"_recipeType":"xxxxx","tapTargets":[],"displayAspectRatio":null}],"editableAccessibilityText":false,"actionTarget":null,"_recipeType":"xxxxx","accessibilityTextAttributes":[],"totalCount":null,"accessibilityText":"xxxxx"},"lazyLoadedActions":{"_recipeType":"xxxxx","_type":"xxxxx","entityUrn":"xxxxx"},"secondarySubtitle":{"textDirection":"xxxxx","_type":"xxxxx","text":"xxxxx","attributesV2":[],"_recipeType":"xxxxx","accessibilityTextAttributesV2":[],"accessibilityText":null},"_type":"xxxxx","navigationUrl":"xxxxx","entityEmbeddedObject":null,"unreadIndicatorDetails":null,"_recipeType":"xxxxx","target":null,"actorTrackingUrn":null,"navigationContext":{"_type":"xxxxx","openExternally":false,"_recipeType":"xxxxx","url":"xxxxx"}},"cluster":null,"bannerCard":null,"searchSuggestionCard":null,"feedbackCard":null,"knowledgeCardV2":null,"keywordsSuggestionCard":null,"queryClarificationCard":null,"simpleText":null,"topicalQuestionCard":null,"simpleTextV2":null,"promoCard":null,"centeredText":null,"simpleImage":null},"position":2,"_recipeType":"xxxxx"}],"results":[],"trackingId":"xxxxx"}]}}} \ No newline at end of file diff --git a/tests/mock-employee-response-last-page b/tests/mock-employee-response-last-page new file mode 100644 index 0000000..45fd86b --- /dev/null +++ b/tests/mock-employee-response-last-page @@ -0,0 +1 @@ +{"data":{"_recipeType":"xxxxx","_type":"xxxxx","searchDashClustersByAll":{"_type":"xxxxx","metadata":{"entityResultAttributes":null,"searchAlert":null,"totalResultCount":null,"_type":"xxxxx","secondaryFilterCluster":null,"_recipeType":"xxxxx","lazyRightRail":null,"queryType":null,"paginationToken":null,"primaryResultType":null,"primaryFilterCluster":null,"blockedQuery":false,"entityActionButtonStyle":null,"searchId":"xxxxx","filterAppliedCount":0,"clusterTitleFontSize":null,"simpleInsightAttributes":null,"knowledgeCardRightRail":null},"paging":{"count":50,"start":1050,"_type":"xxxxx","total":0,"_recipeType":"xxxxx"},"_recipeType":"xxxxx","elements":[]}}} \ No newline at end of file diff --git a/tests/test_linkedin2username.py b/tests/test_linkedin2username.py index 9d37137..0312d16 100644 --- a/tests/test_linkedin2username.py +++ b/tests/test_linkedin2username.py @@ -180,6 +180,7 @@ def test_find_employees(): assert employees[0] == {'full_name': 'Michael Myers', 'occupation': 'Camp Counsellor'} assert employees[1] == {'full_name': 'Freddy Krueger', 'occupation': 'Babysitter'} - result = '{"elements": []}' + with open("tests/mock-employee-response-last-page", "r") as infile: + result = infile.read() assert not linkedin2username.find_employees(result)