-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* adds schulconnex client module * adds new user import endpoint to populate the import user with data from sanis which is using the schulconnex api
- Loading branch information
Showing
164 changed files
with
2,705 additions
and
851 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; | ||
export { SchulconnexClientModule } from './schulconnex-client.module'; | ||
export { SchulconnexRestClient } from './schulconnex-rest-client'; | ||
export { | ||
SanisResponse, | ||
SanisRole, | ||
SanisGroupRole, | ||
SanisGroupType, | ||
SanisGruppenResponse, | ||
SanisResponseValidationGroups, | ||
SanisPersonResponse, | ||
SanisAnschriftResponse, | ||
SanisGruppenzugehoerigkeitResponse, | ||
SanisGruppeResponse, | ||
SanisNameResponse, | ||
SanisOrganisationResponse, | ||
SanisPersonenkontextResponse, | ||
SanisSonstigeGruppenzugehoerigeResponse, | ||
} from './response'; | ||
export { schulconnexResponseFactory } from './testing/schulconnex-response-factory'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { SchulconnexConfigurationMissingLoggable } from './schulconnex-configuration-missing.loggable'; |
16 changes: 16 additions & 0 deletions
16
.../src/infra/schulconnex-client/loggable/schulconnex-configuration-missing.loggable.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { SchulconnexConfigurationMissingLoggable } from './schulconnex-configuration-missing.loggable'; | ||
|
||
describe(SchulconnexConfigurationMissingLoggable.name, () => { | ||
describe('getLogMessage', () => { | ||
it('should return a log message', () => { | ||
const loggable: SchulconnexConfigurationMissingLoggable = new SchulconnexConfigurationMissingLoggable(); | ||
|
||
const logMessage = loggable.getLogMessage(); | ||
|
||
expect(logMessage).toEqual({ | ||
message: | ||
'SchulconnexRestClient: Missing configuration. Please check your environment variables in SCHULCONNEX_CLIENT.', | ||
}); | ||
}); | ||
}); | ||
}); |
4 changes: 2 additions & 2 deletions
4
...ble/user-migration-not-enable.loggable.ts → ...lconnex-configuration-missing.loggable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; | ||
|
||
export class UserMigrationIsNotEnabled implements Loggable { | ||
export class SchulconnexConfigurationMissingLoggable implements Loggable { | ||
getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { | ||
return { | ||
message: 'Feature flag of user migration may be disable or the school is not an LDAP pilot', | ||
message: `SchulconnexRestClient: Missing configuration. Please check your environment variables in SCHULCONNEX_CLIENT.`, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { SchulconnexPersonenInfoParams } from './schulconnex-personen-info-params'; |
13 changes: 13 additions & 0 deletions
13
apps/server/src/infra/schulconnex-client/request/schulconnex-personen-info-params.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export type SchulconnexPropertyContext = 'personen' | 'personenkontexte' | 'organisationen' | 'gruppen' | 'beziehungen'; | ||
|
||
export interface SchulconnexPersonenInfoParams { | ||
vollstaendig?: SchulconnexPropertyContext[]; | ||
|
||
pid?: string; | ||
|
||
'personenkontext.id'?: string; | ||
|
||
'organisation.id'?: string; | ||
|
||
'gruppe.id'?: string; | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions
8
apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { SchulconnexPersonenInfoParams } from './request'; | ||
import { SanisResponse } from './response'; | ||
|
||
export interface SchulconnexApiInterface { | ||
getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise<SanisResponse>; | ||
|
||
getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise<SanisResponse[]>; | ||
} |
30 changes: 30 additions & 0 deletions
30
apps/server/src/infra/schulconnex-client/schulconnex-client.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { OauthAdapterService, OauthModule } from '@modules/oauth'; | ||
import { HttpModule, HttpService } from '@nestjs/axios'; | ||
import { DynamicModule, Global, Module } from '@nestjs/common'; | ||
import { Logger, LoggerModule } from '@src/core/logger'; | ||
import { SchulconnexRestClient } from './schulconnex-rest-client'; | ||
import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; | ||
|
||
@Global() | ||
/** | ||
* @Global is used here to make sure that the module is only instantiated once, with the configuration and can be used in every module. | ||
* Otherwise, you need to import the module with configuration in every module where you want to use it. | ||
*/ | ||
@Module({}) | ||
export class SchulconnexClientModule { | ||
static register(options: SchulconnexRestClientOptions): DynamicModule { | ||
return { | ||
imports: [HttpModule, OauthModule, LoggerModule], | ||
module: SchulconnexClientModule, | ||
providers: [ | ||
{ | ||
provide: SchulconnexRestClient, | ||
useFactory: (httpService: HttpService, oauthAdapterService: OauthAdapterService, logger: Logger) => | ||
new SchulconnexRestClient(options, httpService, oauthAdapterService, logger), | ||
inject: [HttpService, OauthAdapterService, Logger], | ||
}, | ||
], | ||
exports: [SchulconnexRestClient], | ||
}; | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
apps/server/src/infra/schulconnex-client/schulconnex-rest-client-options.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export interface SchulconnexRestClientOptions { | ||
apiUrl: string; | ||
|
||
tokenEndpoint: string; | ||
|
||
clientId: string; | ||
|
||
clientSecret: string; | ||
} |
167 changes: 167 additions & 0 deletions
167
apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { createMock, DeepMocked } from '@golevelup/ts-jest'; | ||
import { OauthAdapterService, OAuthTokenDto } from '@modules/oauth'; | ||
import { HttpService } from '@nestjs/axios'; | ||
import { axiosResponseFactory } from '@shared/testing'; | ||
import { Logger } from '@src/core/logger'; | ||
import { of } from 'rxjs'; | ||
import { SchulconnexConfigurationMissingLoggable } from './loggable'; | ||
import { SanisResponse } from './response'; | ||
import { SchulconnexRestClient } from './schulconnex-rest-client'; | ||
import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; | ||
import { schulconnexResponseFactory } from './testing'; | ||
|
||
describe(SchulconnexRestClient.name, () => { | ||
let client: SchulconnexRestClient; | ||
|
||
let httpService: DeepMocked<HttpService>; | ||
let oauthAdapterService: DeepMocked<OauthAdapterService>; | ||
let logger: DeepMocked<Logger>; | ||
const options: SchulconnexRestClientOptions = { | ||
apiUrl: 'https://schulconnex.url/api', | ||
clientId: 'clientId', | ||
clientSecret: 'clientSecret', | ||
tokenEndpoint: 'https://schulconnex.url/token', | ||
}; | ||
|
||
beforeAll(() => { | ||
httpService = createMock<HttpService>(); | ||
oauthAdapterService = createMock<OauthAdapterService>(); | ||
logger = createMock<Logger>(); | ||
|
||
client = new SchulconnexRestClient(options, httpService, oauthAdapterService, logger); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
describe('constructor', () => { | ||
describe('when configuration is missing', () => { | ||
const setup = () => { | ||
const badOptions: SchulconnexRestClientOptions = { | ||
apiUrl: '', | ||
clientId: '', | ||
clientSecret: '', | ||
tokenEndpoint: '', | ||
}; | ||
return { | ||
badOptions, | ||
}; | ||
}; | ||
|
||
it('should log a message', () => { | ||
const { badOptions } = setup(); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const badOptionsClient = new SchulconnexRestClient(badOptions, httpService, oauthAdapterService, logger); | ||
|
||
expect(logger.debug).toHaveBeenCalledWith(new SchulconnexConfigurationMissingLoggable()); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getPersonInfo', () => { | ||
describe('when requesting person-info', () => { | ||
const setup = () => { | ||
const accessToken = 'accessToken'; | ||
const response: SanisResponse = schulconnexResponseFactory.build(); | ||
|
||
httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); | ||
|
||
return { | ||
accessToken, | ||
response, | ||
}; | ||
}; | ||
|
||
it('should make a request to a SchulConneX-API', async () => { | ||
const { accessToken } = setup(); | ||
|
||
await client.getPersonInfo(accessToken); | ||
|
||
expect(httpService.get).toHaveBeenCalledWith(`${options.apiUrl}/person-info`, { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
'Accept-Encoding': 'gzip', | ||
}, | ||
}); | ||
}); | ||
|
||
it('should return the response', async () => { | ||
const { accessToken, response } = setup(); | ||
|
||
const result: SanisResponse = await client.getPersonInfo(accessToken); | ||
|
||
expect(result).toEqual(response); | ||
}); | ||
}); | ||
|
||
describe('when overriding the url', () => { | ||
const setup = () => { | ||
const accessToken = 'accessToken'; | ||
const customUrl = 'https://override.url/person-info'; | ||
const response: SanisResponse = schulconnexResponseFactory.build(); | ||
|
||
httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); | ||
|
||
return { | ||
accessToken, | ||
customUrl, | ||
}; | ||
}; | ||
|
||
it('should make a request to a SchulConneX-API', async () => { | ||
const { accessToken, customUrl } = setup(); | ||
|
||
await client.getPersonInfo(accessToken, { overrideUrl: customUrl }); | ||
|
||
expect(httpService.get).toHaveBeenCalledWith(customUrl, expect.anything()); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('getPersonenInfo', () => { | ||
describe('when requesting personen-info', () => { | ||
const setup = () => { | ||
const tokens: OAuthTokenDto = new OAuthTokenDto({ | ||
idToken: 'id_token', | ||
accessToken: 'access_token', | ||
refreshToken: 'refresh_token', | ||
}); | ||
const response: SanisResponse[] = schulconnexResponseFactory.buildList(2); | ||
|
||
oauthAdapterService.sendTokenRequest.mockResolvedValueOnce(tokens); | ||
httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); | ||
|
||
return { | ||
tokens, | ||
response, | ||
}; | ||
}; | ||
|
||
it('should make a request to a SchulConneX-API', async () => { | ||
const { tokens } = setup(); | ||
|
||
await client.getPersonenInfo({ 'organisation.id': '1234', vollstaendig: ['personen', 'organisationen'] }); | ||
|
||
expect(httpService.get).toHaveBeenCalledWith( | ||
`${options.apiUrl}/personen-info?organisation.id=1234&vollstaendig=personen%2Corganisationen`, | ||
{ | ||
headers: { | ||
Authorization: `Bearer ${tokens.accessToken}`, | ||
'Accept-Encoding': 'gzip', | ||
}, | ||
} | ||
); | ||
}); | ||
|
||
it('should return the response', async () => { | ||
const { response } = setup(); | ||
|
||
const result: SanisResponse[] = await client.getPersonenInfo({ 'organisation.id': '1234' }); | ||
|
||
expect(result).toEqual(response); | ||
}); | ||
}); | ||
}); | ||
}); |
80 changes: 80 additions & 0 deletions
80
apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { OauthAdapterService, OAuthTokenDto } from '@modules/oauth'; | ||
import { OAuthGrantType } from '@modules/oauth/interface/oauth-grant-type.enum'; | ||
import { ClientCredentialsGrantTokenRequest } from '@modules/oauth/service/dto'; | ||
import { HttpService } from '@nestjs/axios'; | ||
import { Logger } from '@src/core/logger'; | ||
import { AxiosResponse } from 'axios'; | ||
import QueryString from 'qs'; | ||
import { lastValueFrom, Observable } from 'rxjs'; | ||
import { SchulconnexConfigurationMissingLoggable } from './loggable'; | ||
import { SchulconnexPersonenInfoParams } from './request'; | ||
import { SanisResponse } from './response'; | ||
import { SchulconnexApiInterface } from './schulconnex-api.interface'; | ||
import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; | ||
|
||
export class SchulconnexRestClient implements SchulconnexApiInterface { | ||
private readonly SCHULCONNEX_API_BASE_URL: string; | ||
|
||
constructor( | ||
private readonly options: SchulconnexRestClientOptions, | ||
private readonly httpService: HttpService, | ||
private readonly oauthAdapterService: OauthAdapterService, | ||
private readonly logger: Logger | ||
) { | ||
this.checkOptions(); | ||
this.SCHULCONNEX_API_BASE_URL = options.apiUrl; | ||
} | ||
|
||
// TODO: N21-1678 use this in provisioning module | ||
public async getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise<SanisResponse> { | ||
const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/person-info`); | ||
|
||
const response: Promise<SanisResponse> = this.getRequest<SanisResponse>(url, accessToken); | ||
|
||
return response; | ||
} | ||
|
||
public async getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise<SanisResponse[]> { | ||
const token: OAuthTokenDto = await this.requestClientCredentialToken(); | ||
|
||
const url: URL = new URL(`${this.SCHULCONNEX_API_BASE_URL}/personen-info`); | ||
url.search = QueryString.stringify(params, { arrayFormat: 'comma' }); | ||
|
||
const response: Promise<SanisResponse[]> = this.getRequest<SanisResponse[]>(url, token.accessToken); | ||
|
||
return response; | ||
} | ||
|
||
private checkOptions(): void { | ||
if (!this.options.apiUrl || !this.options.clientId || !this.options.clientSecret || !this.options.tokenEndpoint) { | ||
this.logger.debug(new SchulconnexConfigurationMissingLoggable()); | ||
} | ||
} | ||
|
||
private async getRequest<T>(url: URL, accessToken: string): Promise<T> { | ||
const observable: Observable<AxiosResponse<T>> = this.httpService.get(url.toString(), { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}`, | ||
'Accept-Encoding': 'gzip', | ||
}, | ||
}); | ||
|
||
const responseToken: AxiosResponse<T> = await lastValueFrom(observable); | ||
|
||
return responseToken.data; | ||
} | ||
|
||
private async requestClientCredentialToken(): Promise<OAuthTokenDto> { | ||
const { tokenEndpoint, clientId, clientSecret } = this.options; | ||
|
||
const payload: ClientCredentialsGrantTokenRequest = new ClientCredentialsGrantTokenRequest({ | ||
client_id: clientId, | ||
client_secret: clientSecret, | ||
grant_type: OAuthGrantType.CLIENT_CREDENTIALS_GRANT, | ||
}); | ||
|
||
const tokenDto: OAuthTokenDto = await this.oauthAdapterService.sendTokenRequest(tokenEndpoint, payload); | ||
|
||
return tokenDto; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { schulconnexResponseFactory } from './schulconnex-response-factory'; |
Oops, something went wrong.