Skip to content
This repository has been archived by the owner on Sep 6, 2022. It is now read-only.

Commit

Permalink
Add fastlane bot configuration to onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
nakhbari committed Jul 3, 2018
1 parent d4efa33 commit 3f223a4
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 28 deletions.
10 changes: 8 additions & 2 deletions web/app/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export enum LocalStorageKeys {
AUTH_TOKEN = 'auth_token'
}

export type FastlaneStatus =
'failure'|'success'|'ci_problem'|'pending'|'missing_fastfile'|'installing_xcode';
export type FastlaneStatus = 'failure'|'success'|'ci_problem'|'pending'|
'missing_fastfile'|'installing_xcode';

export function fastlaneStatusToEnum(status: FastlaneStatus): BuildStatus {
switch (status) {
Expand All @@ -32,3 +32,9 @@ export function fastlaneStatusToEnum(status: FastlaneStatus): BuildStatus {
throw new Error(`Unknown status type ${status}`);
}
}

/**
* This is what is defined by GitHub to be their token length, but it can be
* modified.
*/
export const GITHUB_API_TOKEN_LENGTH = 40;
48 changes: 47 additions & 1 deletion web/app/onboard/onboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,53 @@ <h6>NOT WORKING - CURRENTLY IN DEVELOPMENT</h6>
</div>
</mat-step>
<mat-step label="Set Up your CI bot account">
CI bot account set up
<p>fastlane.ci needs you to set up a CI bot account to update the build status, etc.</p>
<div class="fci-substep">
<div class="fci-substep-label">
<span class="fci-substep-letter">a</span>
Create an account for your fastlane.ci bot on Github
</div>
</div>
<div class="fci-substep">
<div class="fci-substep-label">
<span class="fci-substep-letter">b</span>
Create a personal access token for use with your bot on Github</div>
<div class="fci-substep-content">
<div>Enable the following permissions
<a href="https://github.com" target="_blank">on Github</a>:</div>
<div class="fci-github-token-scopes-image"></div>
</div>
</div>
<div class="fci-substep">
<div class="fci-substep-label">
<span class="fci-substep-letter">c</span>
Enter information for your fastlane.ci bot
</div>
<div class="fci-substep-content">
<div class="fci-input-container">
<label>CI encryption key</label>
<input formControlName="botToken" placeholder="Enter API token" type="text">
<div class="fci-input-status">
<mat-spinner *ngIf="isFetchingBotEmail" mode="indeterminate" diameter="24"></mat-spinner>
<mat-icon class="fci-success-icon" *ngIf="botEmail">check_circle</mat-icon>
</div>
</div>
<ng-container *ngIf="botEmail">
<div class="fci-input-container">
<label>fastlane.ci username</label>
<span class="fci-username">{{botEmail}}</span>
</div>
<div class="fci-input-container">
<label>fastlane.ci password</label>
<input formControlName="botPassword" placeholder="Enter fastlane.ci password" type="password">
</div>
</ng-container>
</div>
<div>
<button mat-button mat-raised-button color="primary" matStepperNext>Next</button>
<button mat-button mat-raised-button matStepperPrevious>Back</button>
</div>
</div>
</mat-step>
<mat-step label="Create private Repository for ci-config">
Repository setup
Expand Down
44 changes: 44 additions & 0 deletions web/app/onboard/onboard.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,57 @@
}

.mat-stepper-vertical {
.fci-substep {
font-size: 14px;
color: #4a4a4a;
.fci-substep-label {
margin-bottom: 24px;
.fci-substep-letter {
color: #9b9b9b;
border: 1px solid #9b9b9b;
border-radius: 50%;
font-size: 12px;
margin-right: 16px;
padding: 4px 8px;
}
}
.fci-substep-content {
padding-left: 40px;
}
}
.fci-input-container {
padding-bottom: 24px;
.fci-username {
font-size: 16px;
}
.fci-input-status {
display: inline-block;
padding-left: 8px;
.mat-spinner,
.mat-icon {
vertical-align: middle;
display: inline-block;
}
}
}
p {
margin-top: 8px;
margin-bottom: 51px;
font-size: 14px;
color: #4a4a4a;
}
button:first-child {
margin-right: 8px;
}
}

.fci-github-token-scopes-image {
background: url(/assets/github_token_scopes.png);
background-repeat: no-repeat;
background-size: contain;
height: 214px;
width: 595px;
border: 1px solid rgba(0, 0, 0, 0.08);
margin-top: 16px;
margin-bottom: 32px;
}
154 changes: 132 additions & 22 deletions web/app/onboard/onboard.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import {DOCUMENT} from '@angular/common';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule, MatStepperModule} from '@angular/material';
import {MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatStepperModule} from '@angular/material';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {Subject} from 'rxjs/Subject';

import {UserDetails} from '../common/types';
import {DataService} from '../services/data.service';

import {OnboardComponent} from './onboard.component';

const FORM_CONTROL_IDS: string[] = ['encryptionKey'];
const FORM_CONTROL_IDS: string[] = ['encryptionKey', 'botToken', 'botPassword'];
const FORTY_CHAR_STRING: string = new Array(40 + 1).join('a');
const THIRY_NINE_CHAR_STRING: string = new Array(39 + 1).join('a');

describe('OnboardComponent', () => {
let fixture: ComponentFixture<OnboardComponent>;
let document: jasmine.SpyObj<any>;
let component: OnboardComponent;
let dataService: jasmine.SpyObj<Partial<DataService>>;
let userDetailsSubject: Subject<UserDetails>;

beforeEach(() => {
document = {location: {origin: 'fake-host', href: 'fake-href'}};
userDetailsSubject = new Subject<UserDetails>();
dataService = {
getUserDetails:
jasmine.createSpy().and.returnValue(userDetailsSubject.asObservable())
};

TestBed
.configureTestingModule({
imports: [
MatStepperModule, BrowserAnimationsModule, MatButtonModule,
ReactiveFormsModule
ReactiveFormsModule, MatProgressSpinnerModule, MatIconModule
],
declarations: [
OnboardComponent,
],
providers: [
{provide: DataService, useValue: dataService},
]
})
.compileComponents();

Expand All @@ -34,31 +48,127 @@ describe('OnboardComponent', () => {
fixture.detectChanges();
});

for (const control_id of FORM_CONTROL_IDS) {
it(`should have the ${control_id} control properly attached`, () => {
const controlEl: HTMLInputElement =
fixture.debugElement
.query(By.css(`input[formcontrolname="${control_id}"]`))
.nativeElement;
describe('Unit tests', () => {
it('should not get user details when the bot token is 39 chars', () => {
component.form.patchValue({botToken: THIRY_NINE_CHAR_STRING});

expect(dataService.getUserDetails).not.toHaveBeenCalled();
});

it('should get user details when the bot token is 40 chars', () => {
component.form.patchValue({botToken: FORTY_CHAR_STRING});

expect(dataService.getUserDetails)
.toHaveBeenCalledWith(FORTY_CHAR_STRING);
});

it('should clear email if the token is changed', () => {
component.botEmail = 'fake@email.com';
component.form.patchValue({botToken: 'new value'});

expect(component.botEmail).toBeUndefined();
});

it('should set email from user details request', () => {
component.form.patchValue({botToken: FORTY_CHAR_STRING});
userDetailsSubject.next({github: {email: 'best@gmail.com'}});

expect(component.botEmail).toBe('best@gmail.com');
});

it('should set isFetchingBotEmail to false after getting user details',
() => {
component.form.patchValue({botToken: FORTY_CHAR_STRING});
expect(component.isFetchingBotEmail).toBe(true);

userDetailsSubject.next({github: {email: 'best@gmail.com'}});

expect(component.isFetchingBotEmail).toBe(false);
});
});

controlEl.value = '10';
controlEl.dispatchEvent(new Event('input'));
describe('Shallow tests', () => {
let tokenInputEl: HTMLInputElement;

beforeEach(() => {
component.botEmail = 'fake@email.com';
fixture.detectChanges();

expect(component.form.get(control_id).value).toBe('10');
tokenInputEl = fixture.debugElement
.query(By.css('input[formcontrolname="botToken"]'))
.nativeElement;
});

for (const control_id of FORM_CONTROL_IDS) {
it(`should have the ${control_id} control properly attached`, () => {
const controlEl: HTMLInputElement =
fixture.debugElement
.query(By.css(`input[formcontrolname="${control_id}"]`))
.nativeElement;

controlEl.value = '10';
controlEl.dispatchEvent(new Event('input'));
fixture.detectChanges();

component.form.patchValue({[control_id]: '12'});
expect(component.form.get(control_id).value).toBe('10');

component.form.patchValue({[control_id]: '12'});
fixture.detectChanges();

expect(controlEl.value).toBe('12');
});
}

it('should show success check mark if bot email exists', () => {
expect(component.botEmail).toBeDefined();
expect(fixture.debugElement
.queryAll(By.css('.fci-input-status .fci-success-icon'))
.length)
.toBe(1);
});

it('should hide bot email and password if token changes', () => {
expect(fixture.debugElement
.queryAll(By.css('input[formcontrolname="botPassword"]'))
.length)
.toBe(1);
expect(fixture.debugElement.query(By.css('.fci-username'))
.nativeElement.textContent)
.toBe('fake@email.com');

tokenInputEl.value = FORTY_CHAR_STRING;
tokenInputEl.dispatchEvent(new Event('input'));
fixture.detectChanges();

expect(controlEl.value).toBe('12');
expect(fixture.debugElement
.queryAll(By.css('input[formcontrolname="botPassword"]'))
.length)
.toBe(0);
expect(fixture.debugElement.queryAll(By.css('.fci-username')).length)
.toBe(0);
});
}

it('should redirect to old onboarding when button is clicked', () => {
spyOn(component, 'goToOldOnboarding');
fixture.debugElement.query(By.css('.fci-onboard-welcome button'))
.nativeElement.click();
it('should show spinner when looking for bot email', () => {
expect(fixture.debugElement
.queryAll(By.css('.fci-input-status .mat-spinner'))
.length)
.toBe(0);
tokenInputEl.value = FORTY_CHAR_STRING;
tokenInputEl.dispatchEvent(new Event('input'));
fixture.detectChanges();

expect(component.goToOldOnboarding).toHaveBeenCalled();
expect(fixture.debugElement
.queryAll(By.css('.fci-input-status .mat-spinner'))
.length)
.toBe(1);
});

it('should redirect to old onboarding when button is clicked', () => {
spyOn(component, 'goToOldOnboarding');
fixture.debugElement.query(By.css('.fci-onboard-welcome button'))
.nativeElement.click();

expect(component.goToOldOnboarding).toHaveBeenCalled();
});
});
});
20 changes: 20 additions & 0 deletions web/app/onboard/onboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import {DOCUMENT} from '@angular/common';
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';

import {GITHUB_API_TOKEN_LENGTH} from '../common/constants';
import {DataService} from '../services/data.service';


function buildProjectForm(fb: FormBuilder): FormGroup {
return fb.group({
'encryptionKey': ['', Validators.required],
'botToken': ['', Validators.required],
'botPassword': ['', Validators.required],
});
}

Expand All @@ -16,12 +21,27 @@ function buildProjectForm(fb: FormBuilder): FormGroup {
})
export class OnboardComponent {
readonly form: FormGroup;
botEmail: string;
isFetchingBotEmail = false;

constructor(
@Inject(DOCUMENT) private readonly document: any,
private readonly dataService: DataService,
fb: FormBuilder,
) {
this.form = buildProjectForm(fb);

this.form.get('botToken').valueChanges.subscribe((token) => {
delete this.botEmail;
if (token.length === GITHUB_API_TOKEN_LENGTH) {
this.isFetchingBotEmail = true;

this.dataService.getUserDetails(token).subscribe((details) => {
this.botEmail = details.github.email;
this.isFetchingBotEmail = false;
});
}
});
}

goToOldOnboarding() {
Expand Down
7 changes: 4 additions & 3 deletions web/app/onboard/onboard.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule, MatStepperModule} from '@angular/material';
import {MatButtonModule, MatIconModule, MatProgressSpinnerModule, MatStepperModule} from '@angular/material';

import {OnboardComponent} from './onboard.component';

Expand All @@ -14,10 +15,10 @@ import {OnboardComponent} from './onboard.component';
],
imports: [
/** Angular Library Imports */
ReactiveFormsModule,
ReactiveFormsModule, CommonModule,
/** Internal Imports */
/** Angular Material Imports */
MatButtonModule, MatStepperModule,
MatButtonModule, MatStepperModule, MatProgressSpinnerModule, MatIconModule
/** Third-Party Module Imports */
],
providers: [],
Expand Down
Binary file added web/assets/github_token_scopes.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3f223a4

Please sign in to comment.