diff --git a/src/index.ts b/src/index.ts index 74f132e..ca94396 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { MicrosoftPartnerCenter } from './lib/microsoft-partnercenter' -import { MicrosoftGraphApi } from './lib/microsoft-graph-api' +import { MicrosoftGraphApi } from './lib/graph-api/microsoft-graph-api' export default MicrosoftPartnerCenter export { MicrosoftPartnerCenter, MicrosoftGraphApi } diff --git a/src/lib/graph-api/domains/domains.spec.ts b/src/lib/graph-api/domains/domains.spec.ts new file mode 100644 index 0000000..71f5ae2 --- /dev/null +++ b/src/lib/graph-api/domains/domains.spec.ts @@ -0,0 +1,53 @@ +import { Domains } from './domains' +import mockAxios from 'jest-mock-axios' +import { AxiosInstance } from 'axios' +import { DomainDnsRecord } from '../../types' + +describe('Domains', () => { + let domains: Domains + + const data = { id: 'example.com' } + + beforeEach(() => (domains = new Domains(mockAxios as unknown as AxiosInstance))) + + afterEach(() => mockAxios.reset()) + + it('creates a domain', async () => { + jest.spyOn(mockAxios, 'post').mockResolvedValue({ data }) + await expect(domains.createDomain(data.id)).resolves.toEqual(data) + expect(mockAxios.post).toHaveBeenCalledWith('/domains', data) + }) + + it('gets a domain by id', async () => { + jest.spyOn(mockAxios, 'get').mockResolvedValue({ data }) + await expect(domains.getDomain(data.id)).resolves.toEqual(data) + expect(mockAxios.get).toHaveBeenCalledWith(`/domains/${data.id}`) + }) + + it('updates a domain', async () => { + const update = { id: 'new.example.com' } + jest.spyOn(mockAxios, 'patch').mockResolvedValue({ data: update }) + await expect(domains.updateDomain(data.id, update)).resolves.toEqual(update) + expect(mockAxios.patch).toHaveBeenCalledWith(`/domains/${data.id}`, update) + }) + + it('deletes a domain', async () => { + const res = { status: 204 } + jest.spyOn(mockAxios, 'delete').mockResolvedValue(res) + await expect(domains.deleteDomain(data.id)).resolves.toEqual(res) + expect(mockAxios.delete).toHaveBeenCalledWith(`/domains/${data.id}`) + }) + + it('verifies a domain', async () => { + jest.spyOn(mockAxios, 'post').mockResolvedValue({ data }) + await expect(domains.verifyDomain(data.id)).resolves.toEqual(data) + expect(mockAxios.post).toHaveBeenCalledWith(`/domains/${data.id}/verify`) + }) + + it(`verifies a domain's DNS records`, async () => { + const records = [{ id: 'id', ttl: 3600 }] as DomainDnsRecord[] + jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: { value: records } }) + await expect(domains.getDomainVerificationDnsRecords(data.id)).resolves.toEqual(records) + expect(mockAxios.get).toHaveBeenCalledWith(`/domains/${data.id}/verificationDnsRecords`) + }) +}) diff --git a/src/lib/graph-api/domains/domains.ts b/src/lib/graph-api/domains/domains.ts new file mode 100644 index 0000000..3817051 --- /dev/null +++ b/src/lib/graph-api/domains/domains.ts @@ -0,0 +1,81 @@ +import { AxiosInstance } from 'axios' +import { Domain, DomainDnsRecord } from '../../types' + +export class Domains { + constructor(private readonly http: AxiosInstance) {} + + /** + * Create a new domain + * https://learn.microsoft.com/en-us/graph/api/domain-post-domains?view=graph-rest-1.0&tabs=http + * @param domainName The fully qualified name of the domain + * @returns The created Domain object + */ + async createDomain(domainName: string): Promise { + const { data } = await this.http.post('/domains', { id: domainName }) + return data + } + + /** + * List all domains + * https://learn.microsoft.com/en-us/graph/api/domain-list?view=graph-rest-1.0&tabs=http + * @returns An array of Domain objects + */ + async getAllDomains(): Promise { + const { data } = await this.http.get('/domains') + return data.value + } + + /** + * Get a specific domain + * https://learn.microsoft.com/en-us/graph/api/domain-get?view=graph-rest-1.0&tabs=http + * @param domainId The domain ID (which is the fully qualified domain name) + * @returns The Domain object + */ + async getDomain(domainId: string): Promise { + const { data } = await this.http.get(`/domains/${domainId}`) + return data + } + + /** + * Update a domain + * https://learn.microsoft.com/en-us/graph/api/domain-update?view=graph-rest-1.0&tabs=http + * @param domainId The domain ID (which is the fully qualified domain name) + * @param updateData The data to update on the domain + * @returns The updated Domain object + */ + async updateDomain(domainId: string, updateData: Partial): Promise { + const { data } = await this.http.patch(`/domains/${domainId}`, updateData) + return data + } + + /** + * Delete a domain + * https://learn.microsoft.com/en-us/graph/api/domain-delete?view=graph-rest-1.0&tabs=http + * @param domainId The domain ID (which is the fully qualified domain name) + */ + async deleteDomain(domainId: string): Promise { + return this.http.delete(`/domains/${domainId}`) + } + + /** + * Verify a domain + * https://learn.microsoft.com/en-us/graph/api/domain-verify?view=graph-rest-1.0&tabs=http + * @param domainId The domain ID (which is the fully qualified domain name) + * @returns The verified Domain object + */ + async verifyDomain(domainId: string): Promise { + const { data } = await this.http.post(`/domains/${domainId}/verify`) + return data + } + + /** + * Get verification DNS records for a domain + * https://learn.microsoft.com/en-us/graph/api/domain-list-verificationdnsrecords?view=graph-rest-1.0&tabs=http + * @param domainId The domain ID (which is the fully qualified domain name) + * @returns An array of DomainDnsRecord objects + */ + async getDomainVerificationDnsRecords(domainId: string): Promise { + const { data } = await this.http.get(`/domains/${domainId}/verificationDnsRecords`) + return data.value + } +} diff --git a/src/lib/microsoft-graph-api.spec.ts b/src/lib/graph-api/gdap/gdap.spec.ts similarity index 82% rename from src/lib/microsoft-graph-api.spec.ts rename to src/lib/graph-api/gdap/gdap.spec.ts index 09f6609..8bf240b 100644 --- a/src/lib/microsoft-graph-api.spec.ts +++ b/src/lib/graph-api/gdap/gdap.spec.ts @@ -1,4 +1,3 @@ -import { MicrosoftGraphApi } from './microsoft-graph-api' import mockAxios from 'jest-mock-axios' import { CreateGDAPAccessAssignment, @@ -6,19 +5,15 @@ import { GDAPAccessAssignment, GDAPRelationship, UpdateGDAPAccessAssignment, -} from './types/gdap.types' +} from '../../types' +import { Gdap } from './gdap' +import { AxiosInstance } from 'axios' -describe('Microsoft Graph API', () => { - let graphApi: MicrosoftGraphApi +describe('Gdap', () => { + let gdap: Gdap beforeEach(() => { - graphApi = new MicrosoftGraphApi({ - tenantId: 'test', - authentication: { - clientId: 'test', - clientSecret: 'test', - }, - }) + gdap = new Gdap(mockAxios as never as AxiosInstance) }) afterEach(() => { @@ -31,7 +26,7 @@ describe('Microsoft Graph API', () => { const data: CreateGDAPRelationship = { customer: { tenantId: 'customerId' }, } as CreateGDAPRelationship - const result = await graphApi.createGDAPRelationship(data) + const result = await gdap.createGDAPRelationship(data) expect(result).toEqual(relationship) expect(mockAxios.post).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships', @@ -42,7 +37,7 @@ describe('Microsoft Graph API', () => { it('should get all GDAP relationships', async () => { const relationships: GDAPRelationship[] = [{ id: '1' }, { id: '2' }] as GDAPRelationship[] jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: { value: relationships } }) - const result = await graphApi.getAllGDAPRelationships() + const result = await gdap.getAllGDAPRelationships() expect(result).toEqual(relationships) expect(mockAxios.get).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships', @@ -52,7 +47,7 @@ describe('Microsoft Graph API', () => { it('should get a GDAP relationship', async () => { const relationship: GDAPRelationship = { id: '1' } as GDAPRelationship jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: relationship }) - const result = await graphApi.getGDAPRelationship('1') + const result = await gdap.getGDAPRelationship('1') expect(result).toEqual(relationship) expect(mockAxios.get).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/1', @@ -62,7 +57,7 @@ describe('Microsoft Graph API', () => { it('should get GDAP relationships by customer ID', async () => { const relationships: GDAPRelationship[] = [{ id: '1' }, { id: '2' }] as GDAPRelationship[] jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: { value: relationships } }) - const result = await graphApi.getGDAPRelationshipsByCustomerId('customerId') + const result = await gdap.getGDAPRelationshipsByCustomerId('customerId') expect(result).toEqual(relationships) expect(mockAxios.get).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships', @@ -81,7 +76,7 @@ describe('Microsoft Graph API', () => { displayName: 'Updated', } as GDAPRelationship jest.spyOn(mockAxios, 'patch').mockResolvedValue({ data: updatedRelationship }) - const result = await graphApi.updateGDAPRelationship('1', { + const result = await gdap.updateGDAPRelationship('1', { displayName: 'Updated', }) expect(result).toEqual(updatedRelationship) @@ -93,7 +88,7 @@ describe('Microsoft Graph API', () => { it('should delete a GDAP relationship', async () => { jest.spyOn(mockAxios, 'delete').mockResolvedValue({}) - await graphApi.deleteGDAPRelationship('1') + await gdap.deleteGDAPRelationship('1') expect(mockAxios.delete).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/1', ) @@ -105,7 +100,7 @@ describe('Microsoft Graph API', () => { const data: CreateGDAPAccessAssignment = { accessContainer: { id: 'containerId' }, } as never as CreateGDAPAccessAssignment - const result = await graphApi.createGDAPAccessAssignment('relationshipId', data) + const result = await gdap.createGDAPAccessAssignment('relationshipId', data) expect(result).toEqual(assignment) expect(mockAxios.post).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/relationshipId/accessAssignments', @@ -119,7 +114,7 @@ describe('Microsoft Graph API', () => { { id: '2' }, ] as GDAPAccessAssignment[] jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: { value: assignments } }) - const result = await graphApi.getAllGDAPAccessAssignments('relationshipId') + const result = await gdap.getAllGDAPAccessAssignments('relationshipId') expect(result).toEqual(assignments) expect(mockAxios.get).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/relationshipId/accessAssignments', @@ -129,7 +124,7 @@ describe('Microsoft Graph API', () => { it('should get a GDAP access assignment', async () => { const assignment: GDAPAccessAssignment = { id: '1' } as GDAPAccessAssignment jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: assignment }) - const result = await graphApi.getGDAPAccessAssignment('relationshipId', '1') + const result = await gdap.getGDAPAccessAssignment('relationshipId', '1') expect(result).toEqual(assignment) expect(mockAxios.get).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/relationshipId/accessAssignments/1', @@ -145,7 +140,7 @@ describe('Microsoft Graph API', () => { const data: UpdateGDAPAccessAssignment = { accessDetails: { unifiedRoles: ['role1'] }, } as never as UpdateGDAPAccessAssignment - const result = await graphApi.updateGDAPAccessAssignment('relationshipId', '1', data) + const result = await gdap.updateGDAPAccessAssignment('relationshipId', '1', data) expect(result).toEqual(updatedAssignment) expect(mockAxios.patch).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/relationshipId/accessAssignments/1', @@ -155,7 +150,7 @@ describe('Microsoft Graph API', () => { it('should delete a GDAP access assignment', async () => { jest.spyOn(mockAxios, 'delete').mockResolvedValue({}) - await graphApi.deleteGDAPAccessAssignment('relationshipId', '1') + await gdap.deleteGDAPAccessAssignment('relationshipId', '1') expect(mockAxios.delete).toHaveBeenCalledWith( '/tenantRelationships/delegatedAdminRelationships/relationshipId/accessAssignments/1', ) diff --git a/src/lib/microsoft-graph-api.ts b/src/lib/graph-api/gdap/gdap.ts similarity index 60% rename from src/lib/microsoft-graph-api.ts rename to src/lib/graph-api/gdap/gdap.ts index e24d052..7252310 100644 --- a/src/lib/microsoft-graph-api.ts +++ b/src/lib/graph-api/gdap/gdap.ts @@ -1,5 +1,4 @@ -import { MicrosoftApiBase } from './microsoft-api-base' -import { GraphApiConfig } from './types' +import { AxiosInstance } from 'axios' import { CreateGDAPAccessAssignment, CreateGDAPRelationship, @@ -8,13 +7,10 @@ import { GDAPRelationshipRequest, GDAPRelationshipRequestAction, UpdateGDAPAccessAssignment, -} from './types/gdap.types' -import type { Domain, DomainDnsRecord } from './types/domains.types' +} from '../../types' -export class MicrosoftGraphApi extends MicrosoftApiBase { - constructor(config: GraphApiConfig) { - super(config, 'https://graph.microsoft.com/v1.0/', 'https://graph.microsoft.com/.default') - } +export class Gdap { + constructor(private readonly http: AxiosInstance) {} /** * Create a GDAP relationship @@ -23,7 +19,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async createGDAPRelationship(data: CreateGDAPRelationship): Promise { - const { data: gdapRelationship } = await this.httpAgent.post( + const { data: gdapRelationship } = await this.http.post( '/tenantRelationships/delegatedAdminRelationships', data, ) @@ -36,9 +32,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async getAllGDAPRelationships(): Promise { - const { data } = await this.httpAgent.get( - '/tenantRelationships/delegatedAdminRelationships', - ) + const { data } = await this.http.get('/tenantRelationships/delegatedAdminRelationships') return data.value } @@ -49,7 +43,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async getGDAPRelationship(gdapRelationshipId: string): Promise { - const { data } = await this.httpAgent.get( + const { data } = await this.http.get( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}`, ) return data @@ -62,19 +56,16 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async getGDAPRelationshipsByCustomerId(customerId: string): Promise { - const { data } = await this.httpAgent.get( - '/tenantRelationships/delegatedAdminRelationships', - { - params: { - $filter: encodeURIComponent(`customer/tenantId eq '${customerId}'`), - }, - paramsSerializer: (params) => { - return Object.entries(params) - .map(([key, value]) => `${key}=${value}`) - .join('&') - }, + const { data } = await this.http.get('/tenantRelationships/delegatedAdminRelationships', { + params: { + $filter: encodeURIComponent(`customer/tenantId eq '${customerId}'`), }, - ) + paramsSerializer: (params) => { + return Object.entries(params) + .map(([key, value]) => `${key}=${value}`) + .join('&') + }, + }) return data.value } @@ -89,7 +80,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { gdapRelationshipId: string, data: Partial, ): Promise { - const { data: gdapRelationship } = await this.httpAgent.patch( + const { data: gdapRelationship } = await this.http.patch( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}`, data, ) @@ -103,7 +94,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async deleteGDAPRelationship(gdapRelationshipId: string) { - await this.httpAgent.delete( + await this.http.delete( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}`, ) } @@ -112,14 +103,14 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * Create a GDAP relationship request * https://learn.microsoft.com/en-us/graph/api/delegatedadminrelationship-post-requests?view=graph-rest-1.0&tabs=http * @param gdapRelationshipId - * @param data + * @param action * @returns */ async createGDAPRelationshipRequest( gdapRelationshipId: string, action: GDAPRelationshipRequestAction, ): Promise { - const { data: gdapRelationship } = await this.httpAgent.post( + const { data: gdapRelationship } = await this.http.post( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/requests`, { action, @@ -137,7 +128,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { async getAllGDAPRelationshipRequests( gdapRelationshipId: string, ): Promise { - const { data } = await this.httpAgent.get( + const { data } = await this.http.get( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/requests`, ) return data.value @@ -154,7 +145,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { gdapRelationshipId: string, gdapRelationshipRequestId: string, ): Promise { - const { data } = await this.httpAgent.get( + const { data } = await this.http.get( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/requests/${gdapRelationshipRequestId}`, ) return data @@ -163,6 +154,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { /** * Create a GDAP access assignment * https://learn.microsoft.com/en-us/graph/api/delegatedadminrelationship-post-accessassignments?view=graph-rest-1.0&tabs=http + * @param gdapRelationshipId * @param data * @returns */ @@ -170,7 +162,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { gdapRelationshipId: string, data: CreateGDAPAccessAssignment, ): Promise { - const { data: gdapAccessAssignment } = await this.httpAgent.post( + const { data: gdapAccessAssignment } = await this.http.post( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/accessAssignments`, data, ) @@ -184,7 +176,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async getAllGDAPAccessAssignments(gdapRelationshipId: string): Promise { - const { data } = await this.httpAgent.get( + const { data } = await this.http.get( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/accessAssignments`, ) return data.value @@ -201,7 +193,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { gdapRelationshipId: string, gdapAccessAssignmentId: string, ): Promise { - const { data } = await this.httpAgent.get( + const { data } = await this.http.get( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/accessAssignments/${gdapAccessAssignmentId}`, ) return data @@ -220,7 +212,7 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { gdapAccessAssignmentId: string, data: UpdateGDAPAccessAssignment, ): Promise { - const { data: gdapAccessAssignment } = await this.httpAgent.patch( + const { data: gdapAccessAssignment } = await this.http.patch( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/accessAssignments/${gdapAccessAssignmentId}`, data, ) @@ -235,83 +227,8 @@ export class MicrosoftGraphApi extends MicrosoftApiBase { * @returns */ async deleteGDAPAccessAssignment(gdapRelationshipId: string, gdapAccessAssignmentId: string) { - await this.httpAgent.delete( + await this.http.delete( `/tenantRelationships/delegatedAdminRelationships/${gdapRelationshipId}/accessAssignments/${gdapAccessAssignmentId}`, ) } - - /** - * Create a new domain - * https://learn.microsoft.com/en-us/graph/api/domain-post-domains?view=graph-rest-1.0&tabs=http - * @param domainName The fully qualified name of the domain - * @returns The created Domain object - */ - async createDomain(domainName: string): Promise { - const { data } = await this.httpAgent.post('/domains', { id: domainName }) - return data - } - - /** - * List all domains - * https://learn.microsoft.com/en-us/graph/api/domain-list?view=graph-rest-1.0&tabs=http - * @returns An array of Domain objects - */ - async getAllDomains(): Promise { - const { data } = await this.httpAgent.get('/domains') - return data.value - } - - /** - * Get a specific domain - * https://learn.microsoft.com/en-us/graph/api/domain-get?view=graph-rest-1.0&tabs=http - * @param domainId The domain ID (which is the fully qualified domain name) - * @returns The Domain object - */ - async getDomain(domainId: string): Promise { - const { data } = await this.httpAgent.get(`/domains/${domainId}`) - return data - } - - /** - * Update a domain - * https://learn.microsoft.com/en-us/graph/api/domain-update?view=graph-rest-1.0&tabs=http - * @param domainId The domain ID (which is the fully qualified domain name) - * @param updateData The data to update on the domain - * @returns The updated Domain object - */ - async updateDomain(domainId: string, updateData: Partial): Promise { - const { data } = await this.httpAgent.patch(`/domains/${domainId}`, updateData) - return data - } - - /** - * Delete a domain - * https://learn.microsoft.com/en-us/graph/api/domain-delete?view=graph-rest-1.0&tabs=http - * @param domainId The domain ID (which is the fully qualified domain name) - */ - async deleteDomain(domainId: string): Promise { - await this.httpAgent.delete(`/domains/${domainId}`) - } - - /** - * Verify a domain - * https://learn.microsoft.com/en-us/graph/api/domain-verify?view=graph-rest-1.0&tabs=http - * @param domainId The domain ID (which is the fully qualified domain name) - * @returns The verified Domain object - */ - async verifyDomain(domainId: string): Promise { - const { data } = await this.httpAgent.post(`/domains/${domainId}/verify`) - return data - } - - /** - * Get verification DNS records for a domain - * https://learn.microsoft.com/en-us/graph/api/domain-list-verificationdnsrecords?view=graph-rest-1.0&tabs=http - * @param domainId The domain ID (which is the fully qualified domain name) - * @returns An array of DomainDnsRecord objects - */ - async getDomainVerificationDnsRecords(domainId: string): Promise { - const { data } = await this.httpAgent.get(`/domains/${domainId}/verificationDnsRecords`) - return data.value - } } diff --git a/src/lib/graph-api/microsoft-graph-api.ts b/src/lib/graph-api/microsoft-graph-api.ts new file mode 100644 index 0000000..73ed0ab --- /dev/null +++ b/src/lib/graph-api/microsoft-graph-api.ts @@ -0,0 +1,20 @@ +import { MicrosoftApiBase } from '../microsoft-api-base' +import { GraphApiConfig } from '../types' +import { Domains } from './domains/domains' +import { Gdap } from './gdap/gdap' +import { Users } from './users/users' + +export class MicrosoftGraphApi extends MicrosoftApiBase { + domains!: Domains + + gdap!: Gdap + + users!: Users + + constructor(config: GraphApiConfig) { + super(config, 'https://graph.microsoft.com/v1.0/', 'https://graph.microsoft.com/.default') + this.domains = new Domains(this.httpAgent) + this.gdap = new Gdap(this.httpAgent) + this.users = new Users(this.httpAgent) + } +} diff --git a/src/lib/graph-api/users/user.types.ts b/src/lib/graph-api/users/user.types.ts new file mode 100644 index 0000000..e4536de --- /dev/null +++ b/src/lib/graph-api/users/user.types.ts @@ -0,0 +1,100 @@ +export interface GraphUser { + aboutMe: string + accountEnabled: true + ageGroup: string + assignedLicenses: { '@odata.type': 'microsoft.graph.assignedLicense' }[] + assignedPlans: { '@odata.type': 'microsoft.graph.assignedPlan' }[] + birthday: Date + businessPhones: string[] + city: string + companyName: string + consentProvidedForMinor: string + country: string + createdDateTime: Date + creationType: string + customSecurityAttributes: { + '@odata.type': 'microsoft.graph.customSecurityAttributeValue' + } + department: string + displayName: string + employeeHireDate: Date + employeeId: string + employeeOrgData: { '@odata.type': 'microsoft.graph.employeeOrgData' } + employeeType: string + faxNumber: string + givenName: string + hireDate: Date + id: string + identities: { '@odata.type': 'microsoft.graph.objectIdentity' }[] + imAddresses: string[] + interests: string[] + isResourceAccount: false + jobTitle: string + legalAgeGroupClassification: string + licenseAssignmentStates: { '@odata.type': 'microsoft.graph.licenseAssignmentState' }[] + lastPasswordChangeDateTime: Date + mail: string + mailboxSettings: { '@odata.type': 'microsoft.graph.mailboxSettings' } + mailNickname: string + mobilePhone: string + mySite: string + officeLocation: string + onPremisesDistinguishedName: string + onPremisesDomainName: string + onPremisesExtensionAttributes: { + '@odata.type': 'microsoft.graph.onPremisesExtensionAttributes' + } + onPremisesImmutableId: string + onPremisesLastSyncDateTime: Date + onPremisesProvisioningErrors: { '@odata.type': 'microsoft.graph.onPremisesProvisioningError' }[] + onPremisesSamAccountName: string + onPremisesSecurityIdentifier: string + onPremisesSyncEnabled: true + onPremisesUserPrincipalName: string + otherMails: string[] + passwordPolicies: string + passwordProfile: { '@odata.type': 'microsoft.graph.passwordProfile' } + pastProjects: string[] + postalCode: string + preferredDataLocation: string + preferredLanguage: string + preferredName: string + provisionedPlans: { '@odata.type': 'microsoft.graph.provisionedPlan' }[] + proxyAddresses: string[] + responsibilities: string[] + schools: string[] + securityIdentifier: string + serviceProvisioningErrors: { '@odata.type': 'microsoft.graph.serviceProvisioningXmlError' }[] + showInAddressList: true + signInActivity: { '@odata.type': 'microsoft.graph.signInActivity' } + signInSessionsValidFromDateTime: Date + skills: string[] + state: string + streetAddress: string + surname: string + usageLocation: string + userPrincipalName: string + userType: string + calendar: { '@odata.type': 'microsoft.graph.calendar' } + calendarGroups: { '@odata.type': 'microsoft.graph.calendarGroup' }[] + calendarView: { '@odata.type': 'microsoft.graph.event' }[] + calendars: { '@odata.type': 'microsoft.graph.calendar' }[] + contacts: { '@odata.type': 'microsoft.graph.contact' }[] + contactFolders: { '@odata.type': 'microsoft.graph.contactFolder' }[] + createdObjects: { '@odata.type': 'microsoft.graph.directoryObject' }[] + directReports: { '@odata.type': 'microsoft.graph.directoryObject' }[] + drive: { '@odata.type': 'microsoft.graph.drive' } + drives: { '@odata.type': 'microsoft.graph.drive' }[] + events: { '@odata.type': 'microsoft.graph.event' }[] + inferenceClassification: { '@odata.type': 'microsoft.graph.inferenceClassification' } + mailFolders: { '@odata.type': 'microsoft.graph.mailFolder' }[] + manager: { '@odata.type': 'microsoft.graph.directoryObject' } + memberOf: { '@odata.type': 'microsoft.graph.directoryObject' }[] + messages: { '@odata.type': 'microsoft.graph.message' }[] + outlook: { '@odata.type': 'microsoft.graph.outlookUser' } + ownedDevices: { '@odata.type': 'microsoft.graph.directoryObject' }[] + ownedObjects: { '@odata.type': 'microsoft.graph.directoryObject' }[] + photo: { '@odata.type': 'microsoft.graph.profilePhoto' } + photos: { '@odata.type': 'microsoft.graph.profilePhoto' }[] + registeredDevices: { '@odata.type': 'microsoft.graph.directoryObject' }[] +} diff --git a/src/lib/graph-api/users/users.spec.ts b/src/lib/graph-api/users/users.spec.ts new file mode 100644 index 0000000..1c21868 --- /dev/null +++ b/src/lib/graph-api/users/users.spec.ts @@ -0,0 +1,95 @@ +import { Users } from './users' +import mockAxios from 'jest-mock-axios' +import { AxiosInstance } from 'axios' + +describe('Users', () => { + let users: Users + + beforeEach(() => (users = new Users(mockAxios as unknown as AxiosInstance))) + + afterEach(() => mockAxios.reset()) + + it('creates an instance of Users', () => expect(users).toBeTruthy()) + + describe('get', () => { + it('gets a user by id or userPrincipalName', async () => { + const data = { id: 'id', userPrincipalName: 'userPrincipalName' } + jest.spyOn(mockAxios, 'get').mockResolvedValue({ data }) + await expect(users.get('id')).resolves.toEqual(data) + expect(mockAxios.get).toHaveBeenCalledWith('users/id') + }) + }) + + describe('getManager', () => { + it(`gets a user's manager by user's userPrincipalName`, async () => { + const user = { id: 'id', userPrincipalName: 'userPrincipalName' } + const manager = { id: 'id', userPrincipalName: 'managerPrincipalName' } + jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: manager }) + await expect(users.getManager(user.userPrincipalName)).resolves.toEqual(manager) + expect(mockAxios.get).toHaveBeenCalledWith(`users/${user.userPrincipalName}/manager`) + }) + }) + + describe('assignManager', () => { + it('throws an error given user is not found', async () => { + const err = new Error('resource not found') + jest.spyOn(mockAxios, 'get').mockRejectedValue(err) + try { + await users.assignManager('user', 'manager') + expect(true).toBe(false) + } catch (error: any) { + expect(error.message).toEqual( + `${err.message}: Attempted to assign user's manager, but no user was found with userPrincipalName "user"`, + ) + } + }) + + it('throws an error given manager is not found', async () => { + const err = new Error('resource not found') + jest.spyOn(mockAxios, 'get') + .mockResolvedValueOnce({ data: { id: 'userId' } }) + .mockRejectedValueOnce(err) + try { + await users.assignManager('user', 'manager') + expect(true).toBe(false) + } catch (error: any) { + expect(error.message).toEqual( + `${err.message}: Attempted to assign manager as user's manager, but no user was found with userPrincipalName "manager"`, + ) + } + }) + + it('assigns a manager', async () => { + jest.spyOn(mockAxios, 'get') + .mockResolvedValueOnce({ data: { id: 'userId' } }) + .mockResolvedValueOnce({ data: { id: 'managerId' } }) + jest.spyOn(mockAxios, 'put').mockResolvedValue({ status: 204 }) + await users.assignManager('user', 'manager') + expect(mockAxios.put).toHaveBeenCalledWith('users/userId/manager/$ref', { + '@odata.id': 'https://graph.microsoft.com/v1.0/users/managerId', + }) + }) + }) + + describe('removeManager', () => { + it('throws an error given user is not found', async () => { + const err = new Error('resource not found') + jest.spyOn(mockAxios, 'get').mockRejectedValue(err) + try { + await users.removeManager('user') + expect(true).toBe(false) + } catch (error: any) { + expect(error.message).toEqual( + `${err.message}: Attempted to remove user's manager, but no user was found with userPrincipalName "user"`, + ) + } + }) + + it('removes a manager', async () => { + jest.spyOn(mockAxios, 'get').mockResolvedValue({ data: { id: 'userId' } }) + jest.spyOn(mockAxios, 'delete').mockResolvedValue({ status: 204 }) + await users.removeManager('user') + expect(mockAxios.delete).toHaveBeenCalledWith('users/userId/manager/$ref') + }) + }) +}) diff --git a/src/lib/graph-api/users/users.ts b/src/lib/graph-api/users/users.ts new file mode 100644 index 0000000..71150fa --- /dev/null +++ b/src/lib/graph-api/users/users.ts @@ -0,0 +1,84 @@ +import { AxiosInstance, AxiosResponse } from 'axios' +import { GraphUser } from './user.types' + +export class Users { + constructor(private readonly http: AxiosInstance) {} + + /** + * Gets a user by id or userPrincipalName + * https://learn.microsoft.com/en-us/graph/api/user-get + * @param id {id | userPrincipalName} + */ + async get(id: string): Promise { + const { data: user } = await this.http.get(`users/${id}`) + return user + } + + /** + * Gets a user's by user's id or userPrincipalName + * https://learn.microsoft.com/en-us/graph/api/user-list-manager + * @param userPrincipalName + */ + async getManager(userPrincipalName: string): Promise { + const { data: user } = await this.http.get(`users/${userPrincipalName}/manager`) + return user + } + + /** + * Assigns a user's manager + * https://learn.microsoft.com/en-us/graph/api/user-post-manager + * @param userPrincipalName + * @param managerPrincipalName + */ + async assignManager( + userPrincipalName: string, + managerPrincipalName: string, + ): Promise> { + let user: GraphUser + let manager: GraphUser + + try { + user = await this.get(userPrincipalName) + } catch (e: any) { + throw new Error( + `${e.message}: Attempted to assign ${userPrincipalName}'s manager, but no user was found with ` + + `userPrincipalName "${userPrincipalName}"`, + ) + } + + try { + manager = await this.get(managerPrincipalName) + } catch (e: any) { + throw new Error( + `${e.message}: Attempted to assign ${managerPrincipalName} as ${userPrincipalName}'s manager, ` + + `but no user was found with userPrincipalName "${managerPrincipalName}"`, + ) + } + + return this.http.put(`users/${user.id}/manager/$ref`, { + '@odata.id': `https://graph.microsoft.com/v1.0/users/${manager.id}`, + }) + } + + /** + * Removes a user's manager + * https://learn.microsoft.com/en-us/graph/api/user-delete-manager + * @param userPrincipalName + */ + async removeManager( + userPrincipalName: string, + ): Promise> { + let user: GraphUser + + try { + user = await this.get(userPrincipalName) + } catch (e: any) { + throw new Error( + `${e.message}: Attempted to remove ${userPrincipalName}'s manager, but no user was found with ` + + `userPrincipalName "${userPrincipalName}"`, + ) + } + + return this.http.delete(`users/${user.id}/manager/$ref`) + } +}