Merge branch 'dev' into rrt/dev

This commit is contained in:
Aaron Louie 2020-05-23 14:59:57 -04:00
commit abd01ba731
22 changed files with 145 additions and 100 deletions

View File

@ -29,13 +29,20 @@ install:
addons:
chrome: stable
env:
global:
- API_URL=http://localhost:5000/v1.0
- BASE_HREF=/
- HOME_ROUTE=home
- IRB_URL=http://localhost:5001
- PORT0=4200
- PRODUCTION=false
script:
- npm run ci
deploy:
provider: script
script: bash ./deploy.sh
skip_cleanup: true
on:
all_branches: true
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$

View File

@ -14,7 +14,9 @@ RUN npm install && \
npm run build:$build_config
### STAGE 2: Run ###
FROM nginx
FROM nginx:alpine
RUN set -x && apk add --update --no-cache bash libintl gettext curl
COPY --from=builder /crc-bpmn/dist/* /usr/share/nginx/html/
COPY --from=builder /crc-bpmn/nginx.conf /etc/nginx/conf.d/default.conf
@ -23,7 +25,10 @@ COPY ./docker/substitute-env-variables.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Substitute environment variables in nginx configuration and index.html
ENTRYPOINT ["./entrypoint.sh", "/usr/share/nginx/html/index.html,/etc/nginx/conf.d/default.conf", "PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,PORT0"]
ENTRYPOINT ["./entrypoint.sh", \
"/usr/share/nginx/html/index.html,/etc/nginx/conf.d/default.conf", \
"PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,PORT0", \
"/usr/share/nginx/html/index.html"]
### STAGE 3: Profit! ###
CMD ["nginx", "-g", "daemon off;"]

View File

@ -2,11 +2,9 @@ version: "3.3"
services:
db:
container_name: db
image: postgres
volumes:
- ./pg-init-scripts/initdb.sh:/docker-entrypoint-initdb.d/initdb.sh
image: sartography/cr-connect-db:$E2E_TAG
ports:
- "5432"
- "5432:5432"
environment:
- POSTGRES_USER=crc_user
- POSTGRES_PASSWORD=crc_pass
@ -15,29 +13,19 @@ services:
test: ["CMD", "pg_isready"]
timeout: 20s
retries: 10
backend:
container_name: backend
depends_on:
- db
image: sartography/cr-connect-workflow:$E2E_TAG
volumes:
- ./flask-config/config.py:/crc-workflow/instance/config.py
environment:
- FLASK_APP=./crc/__init__.py
- RESET_DB=true
- LDAP_URL=ldap
ports:
- "5000:5000"
command: ./wait-for-it.sh db:5432 -t 0 -- ./docker_run.sh
pb:
container_name: pb
depends_on:
- db
image: sartography/protocol-builder-mock:$E2E_TAG
volumes:
- ./flask-config/config_pb.py:/protocol-builder-mock/instance/config.py
environment:
- FLASK_APP=/protocol-builder-mock/app.py
- UPGRADE_DB=true
- DB_HOST=db
- DB_PORT=5432
- DB_PASSWORD=crc_pass
- DB_USER=crc_user
- DB_NAME=pb_test
ports:
- "5001:5001"
command: ./wait-for-it.sh db:5432 -t 0 -- ./docker_run.sh
@ -47,3 +35,25 @@ services:
image: tuxmonteiro/ldap-mock
ports:
- "3890"
backend:
container_name: backend
depends_on:
- db
- pb
- ldap
image: sartography/cr-connect-workflow:$E2E_TAG
environment:
- FLASK_APP=./crc/__init__.py
- UPGRADE_DB=true
- RESET_DB=true
- LDAP_URL=ldap
- PB_BASE_URL=http://pb:5001/pb/
- DB_HOST=db
- DB_PORT=5432
- DB_PASSWORD=crc_pass
- DB_USER=crc_user
- DB_NAME=crc_test
ports:
- "5000:5000"
command: ./wait-for-it.sh pb:5001 -t 0 -- ./docker_run.sh

View File

@ -1,14 +0,0 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
NAME = "CR Connect Workflow"
DEVELOPMENT = True
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@db:5432/crc_dev"
# %s/%i placeholders expected for uva_id and study_id in various calls.
PB_USER_STUDIES_URL = "http://pb:5001/pb/user_studies?uva_id=%s"
PB_INVESTIGATORS_URL = "http://pb:5001/pb/investigators?studyid=%i"
PB_REQUIRED_DOCS_URL = "http://pb:5001/pb/required_docs?studyid=%i"
PB_STUDY_DETAILS_URL = "http://pb:5001/pb/study?studyid=%i"
print('\n\n*** USING INSTANCE CONFIG ***\n\n')

View File

@ -1,11 +0,0 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
NAME = "CR Connect Protocol Builder Mock"
CORS_ENABLED = False
DEVELOPMENT = True
TESTING = True
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@db:5432/pb_test"
SECRET_KEY = 'a really really really really long secret key'
print('\n\n*** USING INSTANCE CONFIG ***\n\n')

View File

@ -1,22 +0,0 @@
#!/bin/bash
set -e
set -u
function create_user_and_database() {
local database=$1
echo " Creating user and database '$database'"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE USER $database;
CREATE DATABASE $database;
GRANT ALL PRIVILEGES ON DATABASE $database TO $database;
EOSQL
}
if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES"
for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
create_user_and_database $db
done
echo "Multiple databases created"
fi

View File

@ -1,5 +1,13 @@
#!/bin/bash
#####################################################################
# Substitutes the given environment variables in the given files.
# Parameters:
# $1: Comma-delimited list of file paths
# $2: Comma-delimited list of environment variables
# $3: File path to index.html (optional)
#####################################################################
echo 'Substituting environment variables...'
# The first parameter is a comma-delimited list of paths to files which should be substituted
@ -32,5 +40,17 @@ do
echo "$env_var = ${!env_var}"
done
# Execute all other commands with parameters
exec "${@:3}"
# The third parameter is the path to the index.html file
# Rewrite base href in index.html.
# Use @ as a sed delimiter because $BASE_HREF will contain a / character
if [[ -z $3 ]]; then
# Execute all other commands with parameters
exit 0
else
sed -i -e 's@<base href\=\"\/\">@<base href\=\"'"$BASE_HREF"'\">@' "$3"
# Execute all other commands with parameters
exec "${@:4}"
fi

6
package-lock.json generated
View File

@ -12291,9 +12291,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sartography-workflow-lib": {
"version": "0.0.175",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.175.tgz",
"integrity": "sha512-hU86cbCNvj2bGOUftIlOBCbWs6Zl+09g2sUAtuhCP5ZIoZ76xCWAPBUggPGltcEHvQU4Tc1bbMwM8EdfxJuqAQ=="
"version": "0.0.180",
"resolved": "https://registry.npmjs.org/sartography-workflow-lib/-/sartography-workflow-lib-0.0.180.tgz",
"integrity": "sha512-d/ePxdkeIX/ZW3Sp5NIkZsitxRxS560PWorOd9RaojyJtMks2EgCwuN0Gs18NkT0Qrvk7fJHKYYV89k0/H0djQ=="
},
"sass": {
"version": "1.23.3",

View File

@ -21,7 +21,8 @@
"e2e-wf:db-setup": "docker exec -it backend pipenv run flask load-example-data",
"e2e-wf:pb-setup": "docker exec -it pb pipenv run flask db upgrade",
"e2e-wf": "npm run e2e-wf:stop && npm run e2e-wf:clean && npm run e2e-wf:build && npm run e2e-wf:start && npm run e2e-wf:db-upgrade && npm run e2e-wf:db-setup && npm run e2e-wf:pb-setup",
"ci": "npm run lint && npm run test:coverage && npm run e2e:with-wf && sonar-scanner"
"env": "chmod +x ./docker/substitute-env-variables.sh && ./docker/substitute-env-variables.sh src/index.html PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,PORT0 src/index.html",
"ci": "npm run lint && npm run test:coverage && npm run env && npm run e2e:with-wf && sonar-scanner"
},
"private": true,
"dependencies": {
@ -52,7 +53,7 @@
"ngx-file-drop": "^8.0.8",
"ngx-markdown": "^9.0.0",
"rxjs": "~6.5.4",
"sartography-workflow-lib": "^0.0.175",
"sartography-workflow-lib": "^0.0.180",
"tslib": "^1.11.1",
"uuid": "^7.0.2",
"zone.js": "^0.10.3"

View File

@ -6,6 +6,8 @@ import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment, mockFileMeta0} from 'sartography-workflow-lib';
import {OpenFileDialogData} from '../../_interfaces/dialog-data';
@ -15,6 +17,7 @@ describe('OpenFileDialogComponent', () => {
let httpMock: HttpTestingController;
let component: OpenFileDialogComponent;
let fixture: ComponentFixture<OpenFileDialogComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -28,6 +31,7 @@ describe('OpenFileDialogComponent', () => {
MatInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
RouterTestingModule,
],
declarations: [ OpenFileDialogComponent ],
providers: [
@ -48,6 +52,7 @@ describe('OpenFileDialogComponent', () => {
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
{provide: Router, useValue: mockRouter},
]
})
.compileComponents();

View File

@ -6,6 +6,8 @@ import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {FormlyModule} from '@ngx-formly/core';
import {FormlyMaterialModule} from '@ngx-formly/material';
import {ApiService, MockEnvironment, mockWorkflowSpec0, mockWorkflowSpecCategories} from 'sartography-workflow-lib';
@ -17,6 +19,7 @@ describe('WorkflowSpecDialogComponent', () => {
let httpMock: HttpTestingController;
let component: WorkflowSpecDialogComponent;
let fixture: ComponentFixture<WorkflowSpecDialogComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -32,6 +35,7 @@ describe('WorkflowSpecDialogComponent', () => {
MatInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
RouterTestingModule,
],
declarations: [ WorkflowSpecDialogComponent ],
providers: [
@ -52,6 +56,7 @@ describe('WorkflowSpecDialogComponent', () => {
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
{provide: Router, useValue: mockRouter},
]
})
.compileComponents();

View File

@ -1,6 +1,7 @@
import {NgModule} from '@angular/core';
import {APP_BASE_HREF} from '@angular/common';
import {Injectable, NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {SessionRedirectComponent} from 'sartography-workflow-lib';
import {AppEnvironment, SessionRedirectComponent} from 'sartography-workflow-lib';
import {environment} from '../environments/environment.runtime';
import {HomeComponent} from './home/home.component';
import {ModelerComponent} from './modeler/modeler.component';
@ -9,6 +10,14 @@ import {ReferenceFilesComponent} from './reference-files/reference-files.compone
import {SignInComponent} from './sign-in/sign-in.component';
import {SignOutComponent} from './sign-out/sign-out.component';
@Injectable()
export class ThisEnvironment implements AppEnvironment {
homeRoute = environment.homeRoute;
production = environment.production;
api = environment.api;
irbUrl = environment.irbUrl;
baseHref = environment.baseHref;
}
const routes: Routes = [
{
@ -59,7 +68,11 @@ const routes: Routes = [
scrollOffset: [0, 84],
})
],
exports: [RouterModule]
exports: [RouterModule],
providers: [
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
{provide: APP_BASE_HREF, useValue: environment.baseHref},
]
})
export class AppRoutingModule {
}

View File

@ -1,3 +1,4 @@
import {APP_BASE_HREF} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {Injectable, NgModule} from '@angular/core';
import {FlexLayoutModule} from '@angular/flex-layout';
@ -22,6 +23,7 @@ import {FormlyModule} from '@ngx-formly/core';
import {
AppEnvironment,
AuthInterceptor,
ErrorInterceptor,
SartographyFormsModule,
SartographyPipesModule,
SartographyWorkflowLibModule
@ -45,12 +47,12 @@ import {FooterComponent} from './footer/footer.component';
import {HomeComponent} from './home/home.component';
import {ModelerComponent} from './modeler/modeler.component';
import {NavbarComponent} from './navbar/navbar.component';
import {ProtocolBuilderComponent} from './protocol-builder/protocol-builder.component';
import {ReferenceFilesComponent} from './reference-files/reference-files.component';
import {SignInComponent} from './sign-in/sign-in.component';
import {SignOutComponent} from './sign-out/sign-out.component';
import {WorkflowSpecCardComponent} from './workflow-spec-card/workflow-spec-card.component';
import {WorkflowSpecListComponent} from './workflow-spec-list/workflow-spec-list.component';
import { ProtocolBuilderComponent } from './protocol-builder/protocol-builder.component';
import { ReferenceFilesComponent } from './reference-files/reference-files.component';
@Injectable()
export class ThisEnvironment implements AppEnvironment {
@ -58,6 +60,7 @@ export class ThisEnvironment implements AppEnvironment {
production = environment.production;
api = environment.api;
irbUrl = environment.irbUrl;
baseHref = environment.baseHref;
}
@NgModule({
@ -127,11 +130,9 @@ export class ThisEnvironment implements AppEnvironment {
providers: [
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{provide: APP_BASE_HREF, useValue: environment.baseHref},
{provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
]
})
export class AppModule {

View File

@ -2,6 +2,8 @@ import {HttpClientTestingModule, HttpTestingController} from '@angular/common/ht
import {DebugNode} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import * as FileSaver from 'file-saver';
import {ApiService, BPMN_DIAGRAM_DEFAULT, FileType, MockEnvironment} from 'sartography-workflow-lib';
import {
@ -16,17 +18,20 @@ describe('DiagramComponent', () => {
let httpMock: HttpTestingController;
let fixture: ComponentFixture<DiagramComponent>;
let component: DebugNode['componentInstance'];
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
MatIconModule,
RouterTestingModule,
],
declarations: [DiagramComponent],
providers: [
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment}
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: Router, useValue: mockRouter},
]
});

View File

@ -2,6 +2,8 @@ import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {Component} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService, MockEnvironment} from 'sartography-workflow-lib';
import {HomeComponent} from './home.component';
@ -24,6 +26,7 @@ class MockWorkflowSpecListComponent {
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -33,12 +36,14 @@ describe('HomeComponent', () => {
MockWorkflowSpecListComponent,
],
imports: [
HttpClientTestingModule
HttpClientTestingModule,
RouterTestingModule,
],
providers: [
HttpClient,
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: MockEnvironment},
{provide: Router, useValue: mockRouter},
]
})
.compileComponents();

View File

@ -1,6 +1,6 @@
import {Component, OnInit} from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {ApiService, isSignedIn, User} from 'sartography-workflow-lib';
import {ApiService, AppEnvironment, isSignedIn, User, UserParams} from 'sartography-workflow-lib';
interface NavItem {
path?: string;
@ -19,11 +19,11 @@ interface NavItem {
export class NavbarComponent {
navLinks: NavItem[];
user: User;
isSignedIn = isSignedIn;
constructor(
private router: Router,
private api: ApiService,
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
) {
this._loadUser();
}
@ -39,7 +39,6 @@ export class NavbarComponent {
this._loadNavLinks();
}, error => {
localStorage.removeItem('token');
this.api.openUrl('/');
});
}
}

View File

@ -6,6 +6,8 @@ import {MatIconModule} from '@angular/material/icon';
import {MatSnackBarModule} from '@angular/material/snack-bar';
import {BrowserDynamicTestingModule} from '@angular/platform-browser-dynamic/testing';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {of} from 'rxjs';
import {ApiService, FileMeta, FileType, MockEnvironment, mockFileMetaReference0} from 'sartography-workflow-lib';
import {OpenFileDialogComponent} from '../_dialogs/open-file-dialog/open-file-dialog.component';
@ -17,6 +19,7 @@ describe('ReferenceFilesComponent', () => {
let httpMock: HttpTestingController;
let component: ReferenceFilesComponent;
let fixture: ComponentFixture<ReferenceFilesComponent>;
const mockRouter = {navigate: jasmine.createSpy('navigate')};
// Mock file and response headers
const mockDocMeta: FileMeta = createClone()(mockFileMetaReference0);
@ -41,6 +44,7 @@ describe('ReferenceFilesComponent', () => {
MatDialogModule,
MatIconModule,
MatSnackBarModule,
RouterTestingModule,
],
declarations: [
OpenFileDialogComponent,
@ -57,6 +61,7 @@ describe('ReferenceFilesComponent', () => {
}
},
{provide: MAT_DIALOG_DATA, useValue: []},
{provide: Router, useValue: mockRouter},
]
}).overrideModule(BrowserDynamicTestingModule, {
set: {

View File

@ -74,19 +74,19 @@ describe('SignInComponent', () => {
});
it('should fake sign in during testing', () => {
const openSessionSpy = spyOn((component as any).api, 'openSession').and.stub();
const redirectToLoginSpy = spyOn((component as any).api, 'redirectToLogin').and.stub();
(component as any).environment.production = false;
component.model = mockUser;
component.signIn();
expect(openSessionSpy).toHaveBeenCalledWith(mockUser);
expect(redirectToLoginSpy).toHaveBeenCalledWith(jasmine.any(String), mockUser);
expect(component.error).toBeUndefined();
});
it('should display an error if sign in is called on production', () => {
const openSessionSpy = spyOn((component as any).api, 'openSession').and.stub();
const redirectToLoginSpy = spyOn((component as any).api, 'redirectToLogin').and.stub();
(component as any).environment.production = true;
component.signIn();
expect(openSessionSpy).not.toHaveBeenCalled();
expect(redirectToLoginSpy).not.toHaveBeenCalled();
expect(component.error).toBeTruthy();
});

View File

@ -71,8 +71,9 @@ export class SignInComponent implements OnInit {
// For testing purposes, create a user to simulate login.
if (!this.environment.production) {
localStorage.setItem('prev_url', location.origin);
this.model.redirect_url = location.origin + '/session';
this.api.openSession(this.model);
this.api.redirectToLogin(this.model.redirect_url, this.model);
} else {
this.error = new Error('This feature does not work in production.');
}

View File

@ -9,4 +9,5 @@ export const environment: AppEnvironment = {
production: _has(ENV, 'production', '$PRODUCTION') ? (ENV.production === 'true') : false,
api: _has(ENV, 'api', '$API_URL') ? ENV.api : 'http://localhost:5000/v1.0',
irbUrl: _has(ENV, 'irbUrl', '$IRB_URL') ? ENV.irbUrl : 'http://localhost:5001',
baseHref: _has(ENV, 'baseHref', '$BASE_HREF') ? ENV.baseHref : '/',
};

View File

@ -1,3 +1,4 @@
import {AppEnvironment} from 'sartography-workflow-lib';
import {_has, environment} from './environment.runtime';
declare var ENV;
@ -9,6 +10,7 @@ describe('Environments', () => {
expect(environment.api).toEqual('apiRoot');
expect(environment.irbUrl).toEqual('irbUrl');
expect(environment.homeRoute).toEqual('home');
expect(environment.baseHref).toEqual('/');
});
it('should check if environment variables are defined', () => {
@ -17,31 +19,37 @@ describe('Environments', () => {
production: '$PRODUCTION',
api: '$API_URL',
irbUrl: '$IRB_URL',
baseHref: '$BASE_HREF',
};
expect(_has(env, 'homeRoute', '$HOME_ROUTE')).toBeFalse();
expect(_has(env, 'production', '$PRODUCTION')).toBeFalse();
expect(_has(env, 'api', '$API_URL')).toBeFalse();
expect(_has(env, 'irbUrl', '$IRB_URL')).toBeFalse();
expect(_has(env, 'baseHref', '$BASE_HREF')).toBeFalse();
env.homeRoute = undefined;
env.production = undefined;
env.api = undefined;
env.irbUrl = undefined;
env.baseHref = undefined;
expect(_has(env, 'homeRoute', '$HOME_ROUTE')).toBeFalse();
expect(_has(env, 'production', '$PRODUCTION')).toBeFalse();
expect(_has(env, 'api', '$API_URL')).toBeFalse();
expect(_has(env, 'irbUrl', '$IRB_URL')).toBeFalse();
expect(_has(env, 'baseHref', '$BASE_HREF')).toBeFalse();
env.homeRoute = 'something';
env.production = 'something';
env.api = 'something';
env.irbUrl = 'something';
env.baseHref = 'something';
expect(_has(env, 'homeRoute', '$HOME_ROUTE')).toBeTrue();
expect(_has(env, 'production', '$PRODUCTION')).toBeTrue();
expect(_has(env, 'api', '$API_URL')).toBeTrue();
expect(_has(env, 'irbUrl', '$IRB_URL')).toBeTrue();
expect(_has(env, 'baseHref', '$BASE_HREF')).toBeTrue();
});
});

View File

@ -10,6 +10,7 @@
production: '$PRODUCTION',
api: '$API_URL',
irbUrl: '$IRB_URL',
baseHref: '$BASE_HREF',
};
</script>
<meta content="width=device-width, initial-scale=1" name="viewport">