Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions cypress/e2e/fixtures/api/downloadRpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*********************************************************************
* Copyright (c) Intel Corporation 2022
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

const downloadRpc = {
versions: {
success: {
response: [
{
version: 'v3.2.0',
assets: [
{ name: 'rpc_windows_x64.exe', os: 'windows', arch: 'x64' },
{ name: 'rpc_linux_x64.tar.gz', os: 'linux', arch: 'x64' }
]
},
{
version: 'v3.1.0',
assets: [
{ name: 'rpc_linux_x64.tar.gz', os: 'linux', arch: 'x64' },
{ name: 'rpc_windows_x86.exe', os: 'windows', arch: 'x86' }
]
}
]
}
},
profiles: {
success: {
response: {
data: [
{ profileName: 'ccmProfile', activation: 'ccmactivate' },
{ profileName: 'acmProfile', activation: 'acmactivate' }
],
totalCount: 2
}
}
},
domains: {
success: {
response: {
data: [{ profileName: 'corpDomain', domainSuffix: 'corp.example.com' }],
totalCount: 1
}
}
}
}

export { downloadRpc }
153 changes: 153 additions & 0 deletions cypress/e2e/integration/download-rpc/download.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*********************************************************************
* Copyright (c) Intel Corporation 2022
* SPDX-License-Identifier: Apache-2.0
**********************************************************************/

import { httpCodes } from '../../fixtures/api/httpCodes'
import { downloadRpc } from '../../fixtures/api/downloadRpc'
import { RpcRelease } from '../../../../src/models/models'
import { Domain } from '../../../../src/models/models'
import { Profile } from '../../../../src/app/profiles/profiles.constants'

// The Download RPC page only exists in the Console (enterprise) build.
const describeWhenNotCloud = Cypress.expose('CLOUD') ? describe.skip : describe

const osLabels: Record<string, string> = { windows: 'Windows', linux: 'Linux', both: 'Windows and Linux' }

// Mirrors the page: an OS is offered only when the release has an x64 build for it.
const expectedOses = (release: RpcRelease): string[] => {
const hasBuild = (os: string): boolean => release.assets.some((a) => a.os === os && a.arch === 'x64')
return [
'windows',
'linux',
'both'
].filter((os) => (os === 'both' ? hasBuild('windows') && hasBuild('linux') : hasBuild(os)))
}

describeWhenNotCloud('Download RPC', () => {
let releases: RpcRelease[]
let profiles: Profile[]
let domains: Domain[]

beforeEach('Setup and login', () => {
cy.setup()
})

beforeEach('Setup intercepts and open the page', () => {
cy.myIntercept('GET', /api\/package\/rpc-versions$/, {
statusCode: httpCodes.SUCCESS,
body: downloadRpc.versions.success.response
}).as('get-versions')

cy.myIntercept('GET', /admin\/profiles\?\$count=true$/, {
statusCode: httpCodes.SUCCESS,
body: downloadRpc.profiles.success.response
}).as('get-profiles')

cy.myIntercept('GET', /admin\/domains\?\$count=true$/, {
statusCode: httpCodes.SUCCESS,
body: downloadRpc.domains.success.response
}).as('get-domains')

cy.myIntercept('POST', /api\/package$/, {
statusCode: httpCodes.SUCCESS,
headers: { 'content-type': 'application/zip' },
body: 'PK'
}).as('post-package')

cy.goToPage('Download RPC')

cy.wait('@get-versions').then(({ response }) => {
expect(response?.statusCode).to.eq(httpCodes.SUCCESS)
releases = response?.body
expect(releases, 'rpc-go releases').to.have.length.greaterThan(0)
})
cy.wait('@get-profiles').then(({ response }) => {
expect(response?.statusCode).to.eq(httpCodes.SUCCESS)
profiles = response?.body.data
expect(profiles, 'profiles').to.have.length.greaterThan(0)
})
cy.wait('@get-domains').then(({ response }) => {
expect(response?.statusCode).to.eq(httpCodes.SUCCESS)
domains = response?.body.data
})
})

it('shows the preview chip and defaults to the newest version and first profile', () => {
cy.get('[data-cy="previewChip"]').should('contain.text', 'Preview Feature')
cy.get('[data-cy="intro"]').should('be.visible')
cy.then(() => {
cy.matSelectAssert('[data-cy="version"]', releases[0].version)
cy.matSelectAssert('[data-cy="os"]', osLabels[expectedOses(releases[0])[0]])
cy.matSelectAssert('[data-cy="profile"]', profiles[0].profileName)
})
})

it('offers only the operating systems with an x64 build', () => {
cy.then(() => {
releases.forEach((release) => {
cy.matSelectChoose('[data-cy="version"]', release.version)
cy.get('mat-select[data-cy="os"]').click()
cy.get('mat-option').should('have.length', expectedOses(release).length)
expectedOses(release).forEach((os) => cy.get('mat-option').should('contain.text', osLabels[os]))
cy.get('body').type('{esc}')
})
})
})

it('requires a domain for an ACM profile', function () {
const acm = profiles.find((p) => p.activation === 'acmactivate')
if (acm == null) {
this.skip()
}
cy.matSelectChoose('[data-cy="profile"]', acm!.profileName)
cy.get('[data-cy="download"]').should('be.disabled')
cy.get('mat-select[data-cy="domain"]').click()
cy.get('mat-option').first().click()
cy.get('[data-cy="download"]').should('be.enabled')
})

it('warns when the server URL is loopback', () => {
cy.matTextlikeInputType('[data-cy="serverUrl"]', 'https://localhost:8181')
cy.get('[data-cy="serverUrlLocalWarning"]').should('be.visible')
cy.matTextlikeInputType('[data-cy="serverUrl"]', 'https://console.corp.example.com')
cy.get('[data-cy="serverUrlLocalWarning"]').should('not.exist')
})

it('builds a token-authenticated package for every offered operating system', () => {
cy.then(() => {
const profile = profiles[0]
const isAcm = profile.activation === 'acmactivate'
if (isAcm) {
expect(domains, 'domains for an ACM profile').to.have.length.greaterThan(0)
cy.matSelectChoose('[data-cy="domain"]', domains[0].profileName)
}
cy.matTextlikeInputType('[data-cy="serverUrl"]', 'https://console.corp.example.com')
cy.matRadioButtonChoose('[data-cy="authMode"]', 'token')
cy.get('[data-cy="noCredentialsNote"]').should('not.exist')
cy.matSelectChoose('[data-cy="tokenTtl"]', '8 hours')

expectedOses(releases[0]).forEach((os) => {
cy.matSelectChoose('[data-cy="os"]', osLabels[os])
cy.get('[data-cy="download"]').click()
// A live Console fetches and verifies rpc-go from GitHub before zipping.
cy.wait('@post-package', { responseTimeout: 120000 }).then(({ request, response }) => {
expect(request.body).to.deep.equal({
command: 'activate',
version: releases[0].version,
os,
arch: 'x64',
serverUrl: 'https://console.corp.example.com',
auth: { mode: 'token' },
tokenTtl: '8h',
profile: profile.profileName,
...(isAcm ? { domain: domains[0].profileName } : {})
})
expect(response?.statusCode).to.eq(httpCodes.SUCCESS)
expect(response?.headers['content-type']).to.contain('application/zip')
})
cy.get('[data-cy="download"]').should('be.enabled')
})
})
})
})
11 changes: 9 additions & 2 deletions src/app/authorize.interceptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,20 @@ describe('AuthorizeInterceptor', () => {
environment.cloud = originalCloud
})

it('should add if-match header if body contains version', () => {
httpClient.post('/test', { version: '123' }).subscribe()
it('should add if-match header if a PATCH body contains version', () => {
httpClient.patch('/test', { version: '123' }).subscribe()

const req = httpTestingController.expectOne('/test')
expect(req.request.headers.get('if-match')).toBe('123')
})

it('should not add if-match header to a POST whose body contains version', () => {
httpClient.post('/test', { version: 'v3.0.1' }).subscribe()

const req = httpTestingController.expectOne('/test')
expect(req.request.headers.has('if-match')).toBe(false)
})

describe('cloud', () => {
beforeEach(() => {
environment.cloud = true
Expand Down
2 changes: 1 addition & 1 deletion src/app/authorize.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const authorizationInterceptor: HttpInterceptorFn = (request, next) => {
}
}

if ((request.body as any)?.version != null && (request.body as any)?.version !== '') {
if (request.method === 'PATCH' && (request.body as any)?.version != null && (request.body as any)?.version !== '') {
headers['if-match'] = (request.body as any).version
}

Expand Down
8 changes: 8 additions & 0 deletions src/app/core/navbar/navbar.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@
<mat-icon matListItemIcon>devices</mat-icon
><span i18n="navlink">{{ 'devices.headerd.devicesTitle.value' | translate }}</span>
</a>

@if (cloudMode === false) {
<a mat-list-item routerLink="/download-rpc" routerLinkActive="active">
<mat-icon matListItemIcon>download</mat-icon
><span i18n="navlink">{{ 'downloadRpc.title.value' | translate }}</span>
</a>
}

<!-- <a mat-list-item routerLink="/event-channel" routerLinkActive="active">
<mat-icon matListItemIcon>data_exploration</mat-icon><span i18n="navlink">MQTT Events</span>
</a> -->
Expand Down
133 changes: 133 additions & 0 deletions src/app/download-rpc/download-rpc.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<mat-card>
<mat-card-header>
<mat-card-title i18n>{{ 'downloadRpc.title.value' | translate }}</mat-card-title>
<mat-card-subtitle>{{ 'downloadRpc.subtitle.value' | translate }}</mat-card-subtitle>
<mat-chip class="preview-chip" highlighted color="warn" disableRipple data-cy="previewChip">
{{ 'downloadRpc.preview.value' | translate }}
</mat-chip>
</mat-card-header>
@if (isLoading()) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
}
<mat-card-content>
<div class="intro" data-cy="intro">
<p>{{ 'downloadRpc.intro.contents.value' | translate }}</p>
<p>{{ 'downloadRpc.intro.usage.value' | translate }}</p>
</div>
<form [formGroup]="form" class="flex flex-col">
<mat-form-field>
<mat-label>{{ 'downloadRpc.serverUrl.label.value' | translate }}</mat-label>
<input matInput formControlName="serverUrl" autocomplete="off" spellcheck="false" data-cy="serverUrl" />
<mat-hint>{{ 'downloadRpc.serverUrl.hint.value' | translate }}</mat-hint>
@if (form.get('serverUrl')?.hasError('serverUrl')) {
<mat-error>{{ 'downloadRpc.serverUrl.invalid.value' | translate }}</mat-error>
} @else {
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
}
</mat-form-field>

@if (isLoopbackServerUrl()) {
<div class="server-url-warning" data-cy="serverUrlLocalWarning">
<mat-icon color="warn">warning</mat-icon>
<span>{{ 'downloadRpc.serverUrl.localWarning.value' | translate }}</span>
</div>
}

<mat-form-field>
<mat-label>{{ 'downloadRpc.command.label.value' | translate }}</mat-label>
<mat-select formControlName="command" (selectionChange)="onProfileOrCommandChange()" data-cy="command">
@for (c of commands; track c.value) {
<mat-option [value]="c.value">{{ c.label | translate }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.command.hint.value' | translate }}</mat-hint>
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
</mat-form-field>

<mat-form-field>
<mat-label>{{ 'downloadRpc.version.label.value' | translate }}</mat-label>
<mat-select formControlName="version" (selectionChange)="onVersionChange()" data-cy="version">
@for (r of releases(); track r.version) {
<mat-option [value]="r.version">{{ r.version }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.version.hint.value' | translate }}</mat-hint>
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
</mat-form-field>

<mat-form-field>
<mat-label>{{ 'downloadRpc.os.label.value' | translate }}</mat-label>
<mat-select formControlName="os" data-cy="os">
@for (os of availableOses(); track os.value) {
<mat-option [value]="os.value">{{ os.label | translate }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.os.hint.value' | translate }}</mat-hint>
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
</mat-form-field>

@if (form.get('command')?.value === 'activate') {
<mat-form-field>
<mat-label>{{ 'downloadRpc.profile.label.value' | translate }}</mat-label>
<mat-select formControlName="profile" (selectionChange)="onProfileOrCommandChange()" data-cy="profile">
@for (p of profiles(); track p.profileName) {
<mat-option [value]="p.profileName">{{ p.profileName }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.profile.hint.value' | translate }}</mat-hint>
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
</mat-form-field>

@if (isAcmSelected()) {
<mat-form-field>
<mat-label>{{ 'downloadRpc.domain.label.value' | translate }}</mat-label>
<mat-select formControlName="domain" data-cy="domain">
@for (d of domains(); track d.profileName) {
<mat-option [value]="d.profileName">{{ d.profileName }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.domain.hint.value' | translate }}</mat-hint>
<mat-error>{{ REQUIRED_ERROR_KEY | translate }}</mat-error>
</mat-form-field>
}
}

<mat-label>{{ 'downloadRpc.auth.label.value' | translate }}</mat-label>
<mat-radio-group formControlName="authMode" aria-label="auth mode" class="flex flex-col" data-cy="authMode">
@for (m of authMethods; track m.value) {
<mat-radio-button [value]="m.value">{{ m.label | translate }}</mat-radio-button>
}
</mat-radio-group>

@if (form.get('authMode')?.value === 'none') {
<div class="auth-note" data-cy="noCredentialsNote">
<mat-icon>info</mat-icon>
<span>{{ 'downloadRpc.auth.noneNote.value' | translate }}</span>
</div>
}

@if (form.get('authMode')?.value === 'token') {
<mat-form-field>
<mat-label>{{ 'downloadRpc.tokenTtl.label.value' | translate }}</mat-label>
<mat-select formControlName="tokenTtl" data-cy="tokenTtl">
@for (l of tokenLifetimes; track l.value) {
<mat-option [value]="l.value">{{ l.label | translate }}</mat-option>
}
</mat-select>
<mat-hint>{{ 'downloadRpc.tokenTtl.hint.value' | translate }}</mat-hint>
</mat-form-field>
}
</form>
</mat-card-content>
<mat-card-actions>
<button
mat-flat-button
color="primary"
type="button"
[disabled]="form.invalid || isLoading()"
data-cy="download"
(click)="onSubmit()">
{{ 'downloadRpc.download.value' | translate }}
</button>
</mat-card-actions>
</mat-card>
Loading
Loading