diff --git a/.codecov.yml b/.codecov.yml index 3dba42ef37..326dd3e0b2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -17,10 +17,9 @@ coverage: # Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY. patch: default: - # For each PR, make sure the coverage of the new code is within 1% of current overall coverage. - # We let 'patch' be more lenient as we only require *project* coverage to not drop significantly. - target: auto - threshold: 1% + # Enable informational mode, which just provides info to reviewers & always passes + # https://docs.codecov.io/docs/commit-status#section-informational + informational: true # Turn PR comments "off". This feature adds the code coverage summary as a # comment on each PR. See https://docs.codecov.io/docs/pull-request-comments diff --git a/.editorconfig b/.editorconfig index 70ce43b68e..15d4c87b14 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false + +[*.ts] +quote_type = single diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..5c418704b3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,87 @@ +# DSpace Continuous Integration/Build via GitHub Actions +# Concepts borrowed from +# https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-nodejs +name: Build + +# Run this Build for all pushes / PRs to current branch +on: [push, pull_request] + +jobs: + tests: + runs-on: ubuntu-latest + env: + # The ci step will test the dspace-angular code against DSpace REST. + # Direct that step to utilize a DSpace REST service that has been started in docker. + DSPACE_REST_HOST: localhost + DSPACE_REST_PORT: 8080 + DSPACE_REST_NAMESPACE: '/server' + DSPACE_REST_SSL: false + strategy: + # Create a matrix of Node versions to test against (in parallel) + matrix: + node-version: [10.x, 12.x] + # Do NOT exit immediately if one matrix job fails + fail-fast: false + # These are the actual CI steps to perform per job + steps: + # https://github.com/actions/checkout + - name: Checkout codebase + uses: actions/checkout@v1 + + # https://github.com/actions/setup-node + - name: Install Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install latest Chrome (for e2e tests) + run: | + sudo apt-get update + sudo apt-get --only-upgrade install google-chrome-stable -y + google-chrome --version + + # https://github.com/actions/cache/blob/main/examples.md#node---yarn + - name: Get Yarn cache directory + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache Yarn dependencies + uses: actions/cache@v2 + with: + # Cache entire Yarn cache directory (see previous step) + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + # Cache key is hash of yarn.lock. Therefore changes to yarn.lock will invalidate cache + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: ${{ runner.os }}-yarn- + + - name: Install Yarn dependencies + run: yarn install --frozen-lockfile + + - name: Run lint + run: yarn run lint + + - name: Run build + run: yarn run build:prod + + - name: Run specs (unit tests) + run: yarn run test:headless + + # Using docker-compose start backend using CI configuration + # and load assetstore from a cached copy + - name: Start DSpace REST Backend via Docker (for e2e tests) + run: | + docker-compose -f ./docker/docker-compose-ci.yml up -d + docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker container ls + + - name: Run e2e tests (integration tests) + run: yarn run e2e:ci + + - name: Shutdown Docker containers + run: docker-compose -f ./docker/docker-compose-ci.yml down + + # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 + # Upload coverage reports to Codecov (for Node v12 only) + # https://github.com/codecov/codecov-action + - name: Upload coverage to Codecov.io + uses: codecov/codecov-action@v1 + if: matrix.node-version == '12.x' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 39ea1505fc..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -os: linux -dist: bionic -language: node_js - -# Enable caching for yarn & node_modules -cache: - yarn: true - -node_js: - - "10" - - "12" - -# Install latest chrome (for e2e headless testing). Run an update if needed. -addons: - apt: - sources: - - google-chrome - packages: - - google-chrome-stable - update: true - -env: - # The ci step will test the dspace-angular code against DSpace REST. - # Direct that step to utilize a DSpace REST service that has been started in docker. - DSPACE_REST_HOST: localhost - DSPACE_REST_PORT: 8080 - DSPACE_REST_NAMESPACE: '/server' - DSPACE_REST_SSL: false - -before_install: - # Check our versions of everything - - echo "Check versions" - - yarn -v - - docker-compose -v - - google-chrome-stable --version - -install: - # Start up a test DSpace 7 REST backend using the entities database dump - - docker-compose -f ./docker/docker-compose-travis.yml up -d - # Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update - - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli - # Install all local dependencies (retry if initially fails) - - travis_retry yarn install - -before_script: - - echo "Check Docker containers" - - docker container ls - # The following line could be enabled to verify that the rest server is responding. - #- echo "Check REST API available (via Docker)" - #- curl http://localhost:8080/server/ - -script: - # build app and run all tests - - ng lint || travis_terminate 1; - - travis_wait yarn run build:prod || travis_terminate 1; - - yarn test:headless || travis_terminate 1; - - yarn run e2e:ci || travis_terminate 1; - -after_script: - # Shutdown docker after everything runs - - docker-compose -f ./docker/docker-compose-travis.yml down - -# After a successful build and test (see 'script'), send code coverage reports to codecov.io -# These code coverage reports are generated by the codecov node module in our package.json -# NOTE: As there's no need to send coverage multiple times, we only run this for one version of node. -after_success: - - if [ "$TRAVIS_NODE_VERSION" = "12" ]; then codecov; fi diff --git a/README.md b/README.md index 8fec7f404e..4b4c7dc6cf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[](https://travis-ci.com/DSpace/dspace-angular) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal) +[](https://github.com/DSpace/dspace-angular/actions?query=workflow%3ABuild) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal) dspace-angular ============== diff --git a/docker/docker-compose-travis.yml b/docker/docker-compose-ci.yml similarity index 94% rename from docker/docker-compose-travis.yml rename to docker/docker-compose-ci.yml index f0f5ef70e8..f440454bb6 100644 --- a/docker/docker-compose-travis.yml +++ b/docker/docker-compose-ci.yml @@ -1,3 +1,4 @@ +# Docker Compose for running the DSpace backend for e2e testing in CI networks: dspacenet: services: diff --git a/package.json b/package.json index ae4abd2e41..60473fbffc 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "clean:bld": "rimraf build", "clean:node": "rimraf node_modules", "clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld", - "clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env", + "clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node", "clean:env": "rimraf src/environments/environment.ts", "sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts" }, @@ -88,6 +88,7 @@ "debug-loader": "^0.0.1", "deepmerge": "^4.2.2", "express": "4.16.2", + "express-rate-limit": "^5.1.3", "fast-json-patch": "^2.0.7", "file-saver": "^1.3.8", "filesize": "^6.1.0", @@ -137,7 +138,6 @@ "@types/js-cookie": "2.1.0", "@types/lodash": "^4.14.110", "@types/node": "11.15.3", - "codecov": "^3.7.2", "codelyzer": "^5.0.0", "compression-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^5.1.1", diff --git a/scripts/set-env.ts b/scripts/set-env.ts index 0aa106538c..5eee22a4be 100644 --- a/scripts/set-env.ts +++ b/scripts/set-env.ts @@ -54,13 +54,6 @@ import(environmentFilePath) function generateEnvironmentFile(file: GlobalConfig): void { file.production = production; buildBaseUrls(file); - - // TODO remove workaround in beta 5 - if (file.rest.nameSpace.match("(.*)/api/?$") !== null) { - file.rest.nameSpace = getNameSpace(file.rest.nameSpace); - console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${file.rest.nameSpace}'`)); - } - const contents = `export const environment = ` + JSON.stringify(file); writeFile(targetPath, contents, (err) => { if (err) { @@ -119,16 +112,5 @@ function getPort(port: number): string { } function getNameSpace(nameSpace: string): string { - // TODO remove workaround in beta 5 - const apiMatches = nameSpace.match("(.*)/api/?$"); - if (apiMatches != null) { - let newValue = '/' - if (hasValue(apiMatches[1])) { - newValue = apiMatches[1]; - } - return newValue; - } - else { - return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : ''; - } + return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : ''; } diff --git a/server.ts b/server.ts index c640a95ef4..202d5a58bc 100644 --- a/server.ts +++ b/server.ts @@ -28,12 +28,13 @@ import * as compression from 'compression'; import * as cookieParser from 'cookie-parser'; import { join } from 'path'; -import { enableProdMode, NgModuleFactory, Type } from '@angular/core'; +import { enableProdMode } from '@angular/core'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasValue, hasNoValue } from './src/app/shared/empty.util'; +import { hasNoValue, hasValue } from './src/app/shared/empty.util'; +import { UIServerConfig } from './src/config/ui-server-config.interface'; /* * Set path for the browser application's dist folder @@ -121,6 +122,19 @@ function cacheControl(req, res, next) { next(); } +/** + * Checks if the rateLimiter property is present + * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. + */ +if (hasValue((environment.ui as UIServerConfig).rateLimiter)) { + const RateLimit = require('express-rate-limit'); + const limiter = new RateLimit({ + windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, + max: (environment.ui as UIServerConfig).rateLimiter.max + }); + app.use(limiter); +} + /* * Serve static resources (images, i18n messages, …) */ @@ -209,8 +223,9 @@ if (environment.ui.ssl) { certificate: certificate }); } else { + console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] pem.createCertificate({ days: 1, diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts index 34db71db77..8ca02fa8ad 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts @@ -13,6 +13,7 @@ import { Collection } from '../../../../../core/shared/collection.model'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; describe('CollectionAdminSearchResultGridElementComponent', () => { let component: CollectionAdminSearchResultGridElementComponent; @@ -26,6 +27,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => { searchResult.indexableObject = new Collection(); searchResult.indexableObject.uuid = id; } + + const linkService = jasmine.createSpyObj('linkService', { + resolveLink: {} + }); + beforeEach(async(() => { init(); TestBed.configureTestingModule({ @@ -39,6 +45,7 @@ describe('CollectionAdminSearchResultGridElementComponent', () => { providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: {} }, + { provide: LinkService, useValue: linkService} ] }) .compileComponents(); diff --git a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts index 85c81d55a4..6a834dd753 100644 --- a/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts +++ b/src/app/+admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts @@ -14,8 +14,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; import { Community } from '../../../../../core/shared/community.model'; -import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component'; import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths'; +import { LinkService } from '../../../../../core/cache/builders/link.service'; describe('CommunityAdminSearchResultGridElementComponent', () => { let component: CommunityAdminSearchResultGridElementComponent; @@ -29,6 +29,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => { searchResult.indexableObject = new Community(); searchResult.indexableObject.uuid = id; } + + const linkService = jasmine.createSpyObj('linkService', { + resolveLink: {} + }); + beforeEach(async(() => { init(); TestBed.configureTestingModule({ @@ -42,6 +47,7 @@ describe('CommunityAdminSearchResultGridElementComponent', () => { providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: {} }, + { provide: LinkService, useValue: linkService} ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 98552ed40b..beb7413415 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -3,37 +3,41 @@ *ngVar="(collectionRD$ | async) as collectionRD">