From 1213f02b60ab050f051a5dc1e68ee4affb516483 Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Mon, 8 Apr 2024 16:23:21 -0400 Subject: [PATCH 1/5] 354 get candidate names from api --- .../contact-lookup/contact-lookup.component.ts | 13 +++++++------ front-end/src/app/shared/models/candidate.model.ts | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts index 81b71357bb..3c425ccc08 100644 --- a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts +++ b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts @@ -165,17 +165,18 @@ export class ContactLookupComponent extends DestroyerComponent implements OnInit onFecApiCandidateLookupDataSelect(data: FecApiCandidateLookupData) { if (data.candidate_id) { this.fecApiService.getCandidateDetails(data.candidate_id).subscribe((candidate) => { - // TODO: fix once we get info from api and set all names below properly const nameSplit = candidate.name?.split(', '); this.contactLookupSelect.emit( Contact.fromJSON({ type: ContactTypes.CANDIDATE, candidate_id: candidate.candidate_id, - last_name: nameSplit?.[0], - first_name: nameSplit?.[1], - middle_name: '', - prefix: '', - suffix: '', + last_name: (candidate.candidate_first_name && candidate.candidate_last_name ? + candidate.candidate_last_name : nameSplit?.[0]), // namesplit to account for paper filers + first_name: (candidate.candidate_first_name && candidate.candidate_last_name ? + candidate.candidate_first_name : nameSplit?.[1]), // namesplit to account for paper filers + middle_name: candidate.candidate_middle_name, + prefix: candidate.candidate_prefix, + suffix: candidate.candidate_suffix, street_1: candidate.address_street_1, street_2: candidate.address_street_2, city: candidate.address_city, diff --git a/front-end/src/app/shared/models/candidate.model.ts b/front-end/src/app/shared/models/candidate.model.ts index 59e9b5d9e0..68fa24edc7 100644 --- a/front-end/src/app/shared/models/candidate.model.ts +++ b/front-end/src/app/shared/models/candidate.model.ts @@ -8,9 +8,14 @@ export class Candidate extends BaseModel { address_street_1: string | undefined; address_street_2: string | undefined; address_zip: string | undefined; + candidate_first_name: string | undefined; candidate_id: string | undefined; candidate_inactive: boolean | undefined; + candidate_last_name: string | undefined; + candidate_middle_name: string | undefined; + candidate_prefix: string | undefined; candidate_status: string | undefined; + candidate_suffix: string | undefined; cycles: number[] = []; district: string | undefined; district_number: number | undefined; From 33b9de1a07bd6eb9c09997f1911d5cb59ded86c9 Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Wed, 10 Apr 2024 15:48:19 -0400 Subject: [PATCH 2/5] 354 remove extraneous response data --- .../app/shared/services/fec-api.service.spec.ts | 14 ++------------ .../src/app/shared/services/fec-api.service.ts | 10 +++------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/front-end/src/app/shared/services/fec-api.service.spec.ts b/front-end/src/app/shared/services/fec-api.service.spec.ts index c73ed7a3cf..05d0ee0a19 100644 --- a/front-end/src/app/shared/services/fec-api.service.spec.ts +++ b/front-end/src/app/shared/services/fec-api.service.spec.ts @@ -3,10 +3,10 @@ import { TestBed } from '@angular/core/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { CommitteeAccount } from 'app/shared/models/committee-account.model'; import { FecApiPaginatedResponse } from 'app/shared/models/fec-api.model'; +import { Candidate } from '../models/candidate.model'; import { FecFiling } from '../models/fec-filing.model'; import { testMockStore } from '../utils/unit-test.utils'; import { FecApiService } from './fec-api.service'; -import { Candidate } from '../models/candidate.model'; describe('FecApiService', () => { let httpTestingController: HttpTestingController; @@ -28,16 +28,6 @@ describe('FecApiService', () => { describe('#getCandidateDetails()', () => { it('should return candidate details', () => { const candidate: Candidate = new Candidate(); - const response: FecApiPaginatedResponse = { - api_version: '1.0', - pagination: { - page: 1, - per_page: 20, - count: 1, - pages: 1, - }, - results: [candidate], - }; service.getCandidateDetails('P12345678').subscribe((candidateData) => { expect(candidateData).toEqual(candidate); @@ -48,7 +38,7 @@ describe('FecApiService', () => { ); expect(req.request.method).toEqual('GET'); - req.flush(response); + req.flush(candidate); }); }); diff --git a/front-end/src/app/shared/services/fec-api.service.ts b/front-end/src/app/shared/services/fec-api.service.ts index 13970c9cb6..c508147ac3 100644 --- a/front-end/src/app/shared/services/fec-api.service.ts +++ b/front-end/src/app/shared/services/fec-api.service.ts @@ -1,9 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom, map, Observable } from 'rxjs'; +import { Candidate } from '../models/candidate.model'; import { CommitteeAccount } from '../models/committee-account.model'; import { FecApiPaginatedResponse } from '../models/fec-api.model'; -import { Candidate } from '../models/candidate.model'; import { FecFiling } from '../models/fec-filing.model'; import { ApiService } from './api.service'; @@ -16,7 +16,7 @@ export class FecApiService { constructor( private http: HttpClient, private apiService: ApiService, - ) {} + ) { } getHeaders() { return { @@ -33,11 +33,7 @@ export class FecApiService { if (!candidate_id) { throw new Error('Fecfile: No Candidate Id provided in getCandidateDetails()'); } - return this.apiService - .get('/contacts/candidate/', { - candidate_id, - }) - .pipe(map((response: FecApiPaginatedResponse) => (response.results[0] as Candidate) || undefined)); + return this.apiService.get('/contacts/candidate/', { candidate_id }); } /** From 70a358aae8abaae1993791f8d874fb5ec4f1e875 Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Wed, 10 Apr 2024 17:04:17 -0400 Subject: [PATCH 3/5] 354 add test coverage --- .../contact-lookup.component.spec.ts | 90 +++++++++++++++---- .../contact-lookup.component.ts | 2 +- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts index cd8ba023da..3d986febb2 100644 --- a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts +++ b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts @@ -1,18 +1,10 @@ +import { HttpClient } from '@angular/common/http'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EventEmitter } from '@angular/core'; -import { of } from 'rxjs'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { provideMockStore } from '@ngrx/store/testing'; -import { SharedModule } from 'app/shared/shared.module'; -import { ContactService } from 'app/shared/services/contact.service'; -import { testContact, testMockStore } from 'app/shared/utils/unit-test.utils'; -import { DropdownModule } from 'primeng/dropdown'; -import { AutoCompleteModule } from 'primeng/autocomplete'; -import { DialogModule } from 'primeng/dialog'; -import { ContactLookupComponent } from './contact-lookup.component'; -import { LabelPipe } from '../../pipes/label.pipe'; -import { LabelUtils } from 'app/shared/utils/label.utils'; +import { Candidate } from 'app/shared/models/candidate.model'; import { CandidateLookupResponse, CandidateOfficeTypes, @@ -20,14 +12,27 @@ import { Contact, ContactTypeLabels, ContactTypes, + FecApiCandidateLookupData, FecApiCommitteeLookupData, FecfileCandidateLookupData, FecfileCommitteeLookupData, FecfileIndividualLookupData, FecfileOrganizationLookupData, IndividualLookupResponse, - OrganizationLookupResponse, + OrganizationLookupResponse } from 'app/shared/models/contact.model'; +import { ContactService } from 'app/shared/services/contact.service'; +import { FecApiService } from 'app/shared/services/fec-api.service'; +import { SharedModule } from 'app/shared/shared.module'; +import { LabelUtils } from 'app/shared/utils/label.utils'; +import { testContact, testMockStore } from 'app/shared/utils/unit-test.utils'; +import { SelectItem } from 'primeng/api'; +import { AutoCompleteModule } from 'primeng/autocomplete'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { of } from 'rxjs'; +import { LabelPipe } from '../../pipes/label.pipe'; +import { ContactLookupComponent } from './contact-lookup.component'; describe('ContactLookupComponent', () => { let component: ContactLookupComponent; @@ -47,7 +52,14 @@ describe('ContactLookupComponent', () => { AutoCompleteModule, SharedModule, ], - providers: [FormBuilder, ContactService, EventEmitter, provideMockStore(testMockStore)], + providers: [ + FormBuilder, + ContactService, + FecApiService, + EventEmitter, + provideMockStore(testMockStore), + HttpClient, + ], }).compileComponents(); testContactService = TestBed.inject(ContactService); @@ -121,7 +133,7 @@ describe('ContactLookupComponent', () => { tick(500); expect( JSON.stringify(component.contactLookupList) === - JSON.stringify(testCandidateLookupResponse.toSelectItemGroups(true)), + JSON.stringify(testCandidateLookupResponse.toSelectItemGroups(true)), ).toBeTrue(); expect( JSON.stringify([ @@ -183,7 +195,7 @@ describe('ContactLookupComponent', () => { component.onDropdownSearch(testEvent); expect( JSON.stringify(component.contactLookupList) === - JSON.stringify(testCommitteeLookupResponse.toSelectItemGroups(true)), + JSON.stringify(testCommitteeLookupResponse.toSelectItemGroups(true)), ).toBeTrue(); expect( JSON.stringify([ @@ -257,7 +269,7 @@ describe('ContactLookupComponent', () => { tick(500); expect( JSON.stringify(component.contactLookupList) === - JSON.stringify(testOrganizationLookupResponse.toSelectItemGroups()), + JSON.stringify(testOrganizationLookupResponse.toSelectItemGroups()), ).toBeTrue(); expect( JSON.stringify([ @@ -282,6 +294,54 @@ describe('ContactLookupComponent', () => { expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith(testContact); })); + it('#onFecApiCandidateLookupDataSelect happy path', fakeAsync(() => { + const testCandidate = Candidate.fromJSON({ + candidate_id: 'P80000722', + name: 'BIDEN, JOSEPH R JR', + }); + const nameSplit = testCandidate.name?.split(', '); + const eventEmitterEmitSpy = spyOn(component.contactLookupSelect, 'emit'); + const getCandidateDetailsSpy = spyOn(component.fecApiService, + 'getCandidateDetails').and.returnValue(of(testCandidate)); + const testFecApiCandidateLookupData: FecApiCandidateLookupData = { + candidate_id: 'P80000722', + office: 'P', + name: 'BIDEN, JOSEPH R JR', + toSelectItem(): SelectItem { + return { + label: `${this.name} (${this.candidate_id})`, + value: this, + }; + } + } + component.onFecApiCandidateLookupDataSelect(testFecApiCandidateLookupData); + tick(500); + expect(getCandidateDetailsSpy).toHaveBeenCalledOnceWith( + testFecApiCandidateLookupData.candidate_id!); + expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith( + Contact.fromJSON({ + type: ContactTypes.CANDIDATE, + candidate_id: testCandidate.candidate_id, + last_name: (testCandidate.candidate_first_name && testCandidate.candidate_last_name ? + testCandidate.candidate_last_name : nameSplit?.[0]), // namesplit to account for paper filers + first_name: (testCandidate.candidate_first_name && testCandidate.candidate_last_name ? + testCandidate.candidate_first_name : nameSplit?.[1]), // namesplit to account for paper filers + middle_name: testCandidate.candidate_middle_name, + prefix: testCandidate.candidate_prefix, + suffix: testCandidate.candidate_suffix, + street_1: testCandidate.address_street_1, + street_2: testCandidate.address_street_2, + city: testCandidate.address_city, + state: testCandidate.address_state, + zip: testCandidate.address_zip, + employer: '', + occupation: '', + candidate_office: testCandidate.office, + candidate_state: testCandidate.state === 'US' ? '' : testCandidate.state, + candidate_district: testCandidate.district === '00' ? '' : testCandidate.district, + })); + })); + it('#onCreateNewContactSelect Contact happy path', fakeAsync(() => { const eventEmitterEmitSpy = spyOn(component.createNewContactSelect, 'emit'); component.onCreateNewContactSelect(); diff --git a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts index 3c425ccc08..896843754e 100644 --- a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts +++ b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.ts @@ -64,7 +64,7 @@ export class ContactLookupComponent extends DestroyerComponent implements OnInit constructor( private contactService: ContactService, - private fecApiService: FecApiService, + public fecApiService: FecApiService, ) { super(); } From 264fe5f9a9439a602c793eb72aad2c1faa4d82cc Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Wed, 10 Apr 2024 17:20:13 -0400 Subject: [PATCH 4/5] 354 lint --- .../components/contact-lookup/contact-lookup.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts index 3d986febb2..acbb016b48 100644 --- a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts +++ b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts @@ -317,7 +317,7 @@ describe('ContactLookupComponent', () => { component.onFecApiCandidateLookupDataSelect(testFecApiCandidateLookupData); tick(500); expect(getCandidateDetailsSpy).toHaveBeenCalledOnceWith( - testFecApiCandidateLookupData.candidate_id!); + testFecApiCandidateLookupData.candidate_id!); // eslint-disable-line @typescript-eslint/no-non-null-assertion expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith( Contact.fromJSON({ type: ContactTypes.CANDIDATE, From 3181d2b8fafaf563b5b1203905bbabc619c562e0 Mon Sep 17 00:00:00 2001 From: David Heitzer Date: Tue, 16 Apr 2024 13:11:53 -0400 Subject: [PATCH 5/5] 354 unit test --- .../contact-lookup.component.spec.ts | 82 +++++++++++++++---- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts index acbb016b48..ef6cf071ee 100644 --- a/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts +++ b/front-end/src/app/shared/components/contact-lookup/contact-lookup.component.spec.ts @@ -294,12 +294,11 @@ describe('ContactLookupComponent', () => { expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith(testContact); })); - it('#onFecApiCandidateLookupDataSelect happy path', fakeAsync(() => { + it('#onFecApiCandidateLookupDataSelect candidate name only', fakeAsync(() => { const testCandidate = Candidate.fromJSON({ candidate_id: 'P80000722', name: 'BIDEN, JOSEPH R JR', }); - const nameSplit = testCandidate.name?.split(', '); const eventEmitterEmitSpy = spyOn(component.contactLookupSelect, 'emit'); const getCandidateDetailsSpy = spyOn(component.fecApiService, 'getCandidateDetails').and.returnValue(of(testCandidate)); @@ -321,24 +320,71 @@ describe('ContactLookupComponent', () => { expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith( Contact.fromJSON({ type: ContactTypes.CANDIDATE, - candidate_id: testCandidate.candidate_id, - last_name: (testCandidate.candidate_first_name && testCandidate.candidate_last_name ? - testCandidate.candidate_last_name : nameSplit?.[0]), // namesplit to account for paper filers - first_name: (testCandidate.candidate_first_name && testCandidate.candidate_last_name ? - testCandidate.candidate_first_name : nameSplit?.[1]), // namesplit to account for paper filers - middle_name: testCandidate.candidate_middle_name, - prefix: testCandidate.candidate_prefix, - suffix: testCandidate.candidate_suffix, - street_1: testCandidate.address_street_1, - street_2: testCandidate.address_street_2, - city: testCandidate.address_city, - state: testCandidate.address_state, - zip: testCandidate.address_zip, + candidate_id: 'P80000722', + last_name: 'BIDEN', + first_name: 'JOSEPH R JR', + middle_name: undefined, + prefix: undefined, + suffix: undefined, + street_1: undefined, + street_2: undefined, + city: undefined, + state: undefined, + zip: undefined, employer: '', occupation: '', - candidate_office: testCandidate.office, - candidate_state: testCandidate.state === 'US' ? '' : testCandidate.state, - candidate_district: testCandidate.district === '00' ? '' : testCandidate.district, + candidate_office: undefined, + candidate_state: undefined, + candidate_district: undefined, + })); + })); + + it('#onFecApiCandidateLookupDataSelect candidate last_name and first_name', fakeAsync(() => { + const testCandidate = Candidate.fromJSON({ + candidate_id: 'P80000722', + candidate_first_name: 'test_candidate_first_name', + candidate_last_name: 'test_candidate_last_name', + candidate_middle_name: 'test_candidate_middle_name', + candidate_prefix: 'test_candidate_prefix', + candidate_suffix: 'test_candidate_suffix', + }); + const eventEmitterEmitSpy = spyOn(component.contactLookupSelect, 'emit'); + const getCandidateDetailsSpy = spyOn(component.fecApiService, + 'getCandidateDetails').and.returnValue(of(testCandidate)); + const testFecApiCandidateLookupData: FecApiCandidateLookupData = { + candidate_id: 'P80000722', + office: 'P', + name: 'BIDEN, JOSEPH R JR', + toSelectItem(): SelectItem { + return { + label: `${this.name} (${this.candidate_id})`, + value: this, + }; + } + } + component.onFecApiCandidateLookupDataSelect(testFecApiCandidateLookupData); + tick(500); + expect(getCandidateDetailsSpy).toHaveBeenCalledOnceWith( + testFecApiCandidateLookupData.candidate_id!); // eslint-disable-line @typescript-eslint/no-non-null-assertion + expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith( + Contact.fromJSON({ + type: ContactTypes.CANDIDATE, + candidate_id: 'P80000722', + last_name: 'test_candidate_last_name', + first_name: 'test_candidate_first_name', + middle_name: 'test_candidate_middle_name', + prefix: 'test_candidate_prefix', + suffix: 'test_candidate_suffix', + street_1: undefined, + street_2: undefined, + city: undefined, + state: undefined, + zip: undefined, + employer: '', + occupation: '', + candidate_office: undefined, + candidate_state: undefined, + candidate_district: undefined, })); }));