My implementation is based on this article: https://dev.to/nestjs/advanced-nestjs-how-to-build-completely-dynamic-nestjs-modules-1370
I want to test my generic, Twilio-based SMS sender service that I share between multiple parts of my application. I want to configure it when I'm importing it from somewhere else, so I'm writing it as a dynamic module. On top of that, the options that I pass to the dynamic module are themselves constructed dynamically, they are read from my .env file. I'm using the factory pattern when I'm registering my provider:
// app.module.ts
#Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: [
'.env',
],
validationSchema,
}),
SharedSmsModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService<EnvironmentVariables>) => {
return {
accountSid: configService.get('TWILIO_ACCOUNT_SID'),
authToken: configService.get('TWILIO_AUTH_TOKEN'),
smsSenderPhoneNumber: configService.get(
'TWILIO_SMS_SENDER_PHONE_NUMBER'
),
};
},
}),
],
})
export class AppModule {}
My shared-sms module calls the function provided in the registerAsync method in app.module.ts:
// shared-sms.module.ts
export interface SharedSmsModuleOptions {
accountSid: string;
authToken: string;
smsSenderPhoneNumber: string;
}
export interface SharedSmsModuleAsyncOptions extends ModuleMetadata {
imports: any[];
inject: any[];
useFactory?: (
...args: any[]
) => Promise<SharedSmsModuleOptions> | SharedSmsModuleOptions;
}
#Module({})
export class SharedSmsModule {
static registerAsync(
sharedSmsModuleAsyncOptions: SharedSmsModuleAsyncOptions
): DynamicModule {
return {
global: true,
module: SharedSmsModule,
imports: sharedSmsModuleAsyncOptions.imports,
providers: [
{
provide: 'SHARED_SMS_OPTIONS',
useFactory: sharedSmsModuleAsyncOptions.useFactory,
inject: sharedSmsModuleAsyncOptions.inject || [],
},
SharedSmsService,
],
exports: [SharedSmsService],
};
}
}
Now I have access to the options variables in my shared-sms.service:
// shared-sms.service
#Injectable()
export class SharedSmsService {
private twilioClient: Twilio;
constructor(
#Inject('SHARED_SMS_OPTIONS') private options: SharedSmsModuleOptions
) {
this.twilioClient = new Twilio(
this.options.accountSid,
this.options.authToken
);
}
async sendSms(sendSmsDto: SendSmsDto): Promise<MessageInstance> {
await validateOrReject(plainToInstance(SendSmsDto, sendSmsDto));
const smsData = {
from: this.options.smsSenderPhoneNumber,
to: sendSmsDto.to,
body: sendSmsDto.body,
};
return await this.twilioClient.messages.create(smsData);
}
}
So long everything seems to be working. But I'm having issues when I'm trying to test the service's sendSms function. I can write tests that work when I'm providing hardcoded Twilio test account values in my test file. But I don't want to commit them to the repository, so I would want to get them from my .env file. I have tried providing everything to the Test.createTestingModule function when I'm creating my moduleRef, based on what I did in the code that I already wrote, but I couldn't specify the Twilio test account values dynamically. As I don't see documentation regarding this issue, I feel like that I'm either missing a conceptual point (providing so many things in the test seems like an overkill) or there is a trivial work-around. Please help me figure out how to pass those values to my tests from my .env file
I am have an issue with this package #ntegral/nestjs-sentry in nestjs. I have a custom logger I use in my application
#Injectable()
export class CustomLogger implements LoggerService {
constructor(#InjectSentry() private readonly client: SentryService) {}
log(message: any, ...optionalParams: any[]) {
this.client.instance().captureMessage(message, ...optionalParams);
}
}
I then inject the into User Controller and in the user.controller.spec.ts
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
CustomLogger,
UsersService,
SentryService,
],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
I get this error
FAIL src/users/users.controller.spec.ts (9.449 s)
● UsersController › should be defined
Nest can't resolve dependencies of the CustomLogger (?). Please make sure that the argument Symbol(SentryToken) at index [0] is available in the RootTestModule context.
Potential solutions:
- If Symbol(SentryToken) is a provider, is it part of the current RootTestModule?
- If Symbol(SentryToken) is exported from a separate #Module, is that module imported within RootTestModule?
#Module({
imports: [ /* the Module containing Symbol(SentryToken) */ ]
})
I have tried adding the SentryService to the spec providers but that does not fix the error. Has anyone encountered this and how did you fix it.
I run in exactly the same issue. It seemed to be that the library uses a different token for its own Inject annotation. I was able to fix it in my tests by using the provided token for the SentryService mock.
import { SENTRY_TOKEN } from '#ntegral/nestjs-sentry';
// ...
const module: TestingModule = await Test.createTestingModule({
providers: [
// ...
{
provide: SENTRY_TOKEN,
useValue: { debug: jest.fn() }, // provide SentryService Mock here
},
],
})
I need to add an additional url parameter (which is dynamic and not fixed) to the fields calls that happen on the PDP page. I have tried extending the the product service but that doesnt fire any of my overide functions.
I have now ended up implementing the product adapter so I just want to confirm this is 100% correct.
export class MyProductAdapter implements ProductAdapter {
if you want to add a (fixed) value in your fields call you can override the default call and add your missing value. Create a file yourOccProductDetails.config.ts
export const yourOccProductDetailsConfig: OccConfig = {
backend: {
occ: {
endpoints: {
product: {
details: 'products/${productCode}?fields=averageRating,stock(DEFAULT),description,availableForPickup,code,url,price(DEFAULT),numberOfReviews,manufacturer,categories(FULL),priceRange,multidimensional,tags,images(FULL),yourParam',
},
},
},
},
},
And in your module add the config to your providers array
import { yourOccProductDetailsConfig } from './yourOccProductDetails.config'
#NgModule({
imports: [...],
declarations: [YourProductDetailsComponent],
exports: [YourProductDetailsComponent],
providers: [provideConfig(yourOccProductDetailsConfig)],
})
export class YourProductDetailsModule {}
Here is the documentation part for a problem you have faced:
https://sap.github.io/spartacus-docs/connecting-to-other-systems/#configuring-endpoints
i'm working on an Angular project. I've firstly add translaService in the project and everything were good. The language were shared by all components correctly.
When i've enable the lazy loading, i have divided my project by modules (this was expected) and i've notice that the translated language were not shared anymore accross modules. For translation, i use ngx-translate.
in app.module i have this:
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (httpTranslateLoader),
deps: [HttpClient]
},
isolate:false
})
export function httpTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
in my shared module i have this:
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
}
#NgModule({
declarations: [],
imports: [
CommonModule,
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
},
isolate: false,
extend:true
})
]
,
exports:[TranslateModule]
})
export class SharedTranslateModule {
constructor( translate: TranslateService) {
translate.addLangs(['en', 'fr']);
translate.setDefaultLang('en');
translate.use('en');
}
}
in my about module i have this:
TranslateModule.forChild({
loader: {
provide: TranslateLoader,
useFactory: (aboutHttpTranslateLoader),
deps: [HttpClient]
},
isolate: false
})
export function aboutHttpTranslateLoader(http : HttpClient){
return new TranslateHttpLoader(http, './assets/i18n/about/', '.json')
}
my project is divide like this
|app.component.html
<app-header> </app-header>
<router-outlet> <router-outlet>
<app-footer> </app-footer>
app-header is the component where i switch languages by using translate.use(selected-language)
I'm out of ideas. i have try many things but they doesn't solve the problem.
Can someone help me?
Thank you!
EDIT : Obviously this is outdated, now you provide your guard at the providers array in an NgModule. Watch other answers or official documentation for more information.
bootstrapping on a component is outdated
provideRouter() is outdated as well
I'm trying to setup Authentication in my project, using a login and AuthGuard from the Angular2 guide : https://angular.io/docs/ts/latest/guide/router.html
I'm using the release : "#angular/router": "3.0.0-beta.1".
I'll try to explain as much as possible, feel free to tell me if you need more details.
I have my main.ts file which boostraps the app with the following code :
bootstrap(MasterComponent, [
APP_ROUTER_PROVIDERS,
MenuService
])
.catch(err => console.error(err));
I load the MasterComponent, which loads a Header containing buttons that allow me to navigate through my app and it also contains my main for now.
I'm following the guide to make my app work the same way, with the following app.routes.ts :
export const routes: RouterConfig = [
...LoginRoutes,
...MasterRoutes
];
export const APP_ROUTER_PROVIDERS = [
provideRouter(routes),
AUTH_PROVIDERS
];
And the login.routes.ts from the guide, which defines my AuthGuard :
export const LoginRoutes = [
{ path: 'login', component: LoginComponent }
];
export const AUTH_PROVIDERS = [AuthGuard, AuthService];
my Master component has its own route definition, which also contains the guard I'm trying to setup. master.routes.ts :
export const MasterRoutes : RouterConfig = [
{ path: '', redirectTo: '/accueil', pathMatch: 'full' },
{
path: 'accueil',
component: AccueilComponent
},
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
];
And I'm using the same files as the guide, which are auth.guard.ts, auth.service.ts, login.component.ts and login.routes.ts.
In my header.component.ts file, when I try to access any routes, it's working just fine, but when I try to access the guarded path (/dashboard), I get the No provider for AuthGuard! error.
I saw the recent post with the same issue as mine (NoProviderError using CanActivate in Angular 2), but to me the guard is bootstraped correctly up to the main.ts file, so my router should know which routes should be provided with the AuthGuard right ?
Any help or advice would be greatly appreciated.
Thanks !
I had this same issue after going through the Route Guards section of Routing and Authorization tutorial on the Angular website https://angular.io/docs/ts/latest/guide/router.html, it is section 5.
I am adding AuthGuard to one of my main routes and not to child routes like the tutorial shows.
I fixed it by added AuthGuard to my list of providers in my app.module.ts file, so that file now looks like this:
import { AppComponent } from './app.component';
import {AppRoutingModule} from './app-routing.module';
import {AuthGuard} from './auth-gaurd.service';
import { AnotherPageComponent } from './another-page/another-page.component';
import { LoginPageComponent } from './login-page/login-page.component';
#NgModule({
imports: [
BrowserModule,
FormsModule,
JsonpModule,
AppRoutingModule,
HttpModule
],
declarations: [
AppComponent,
LoginPageComponent,
AnotherPageComponent
],
providers: [AuthGuard],
bootstrap: [AppComponent]
})
export class AppModule { }
I have gone back through the tutorial and in their app.module.ts file, they do not add AuthGuard to the providers, not sure why.
Try to add
#Injectable({
providedIn: 'root'
})
no need to add to module provider.
Also, don't fall into the trap of using a literal for the guard class inside your routing configuration, just because some blog articles do:
{ path: 'whatever', component: WhatEverComponent, canActivate: ['WhatEverGuard'] }
is not going to work (No provider for...), instead, use the class directly:
{ path: 'whatever', component: WhatEverComponent, canActivate: [WhatEverGuard] }
Another hint, when lazy loading components, the guard is applied in the routing configuration of the parent component, not in the routing configuration of the lazy loaded component.
For those who still have this error - don't forget to include your AuthGuard service or class to main bootstrap function.
And don't forget to import this service before bootstrap runs.
import { bootstrap } from '#angular/platform-browser-dynamic';
import { AppComponent } from './app.component';
import { AuthGuard } from './shared/auth.service';
bootstrap(AppComponent, [
appRouterProviders,
AuthGuard
]);
Angular 2 team did not mention this in main router docs, and it took couple of hours for me to figure it out.
The answer is further down in the tutorial. See the file listings in the "Add the LoginComponent" topic under the "Component-less route:..." section in "Milestone 5: Route Guards". It shows AuthGuard and AuthService being imported and added to the providers array in login-routing.module.ts, and then that module being imported into app.module.ts.
login-routing.module.ts
...
import { AuthGuard } from './auth-guard.service';
import { AuthService } from './auth.service';
...
#NgModule({
...
providers: [
AuthGuard,
AuthService
]
})
export class LoginRoutingModule {}
app.module.ts
import { LoginRoutingModule } from './login-routing.module';
#NgModule({
imports: [
...
LoginRoutingModule,
...
],
...
providers: [
DialogService
],
...
Actually, it was only a typo in an import...
I was typing
import { AuthGuard } from './../Authentification/auth.guard';
instead of
import { AuthGuard } from './../authentification/auth.guard';
making it not working but at the same time not displaying me any error...
(sadface)
I encountered this issue when I was following a tutorial. I tried most of the answer here but not getting any success. Then I tried the silly way like putting the AuthGuard before the other services in the provider and it works.
// app.module.ts
..
providers: [
AuthGuard,
UserService,
ProjectService
]
Since you got the solution as it was due to syntax issue. I just wanted to share this info.
we need to provide the AuthGaudSerivce as provider in only that module that correspond to respective route. No need to provide in main module or root module as main module will automatically load all the given sub module.This helps in keeping the code modular and encapsulated.
for example, suppose we have below scenario
1. we have module m1
2. we have route m1r in module m1
3. route m1r has 2 route r1 and r2
4. we want to protect r1 using authGaurd
5. finally we have main module that is dependent on sub module m1
Below is just prototype, not the actual code for understanding purpose
//m1.ts
import {AuthGaurd} from './auth.gaurd.service'
import {m1r} from './m1r'
#NgModule(
imports: [m1r],
providers: [AuthGaurd]
)
export class m1{
}
//m1r.ts
import {AuthGaurd} from './auth.gaurd.service'
const authRoute = [
{path: '/r1', component: 'authComponent', canActivate: [AuthGaurd]},
{path: '/r2', component: 'other'}
]
export authRoute
//main.module.ts
import {m1} from ''
import {mainComponent} from ''
#NgModule({
imports: [m1],
bootstrap: [mainComponent]
})
export class MainModule{}
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (localStorage.getItem('currentUser')) {
// logged in so return true
return true;
}
// not logged in so redirect to login page with the return url
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
return false;
}
}
Importing both HttpModule and HttpClientModule helped me.
import { HttpClientModule } from '#angular/common/http';
import { HttpModule } from '#angular/http';
you can try import AuthGuard in provider of that module and then import it in the routing component-routing.module.ts file also
#NgModule({
providers: [
AuthGuard
],})
This happened to me when I had setup my Routes incorrectly:
WRONG
const routes: Routes =
[
{
path: 'my-path',
component: MyComponent,
resolve: { myList: MyListResolver, canActivate: [ AuthenticationGuard ] }
},
];
Note that in this case canActivate was accidentally made a part of the resolve object.
CORRECT
const routes: Routes =
[
{
path: 'my-path',
component: MyComponent,
resolve: { myList: MyListResolver },
canActivate: [ AuthenticationGuard ]
},
];