This commit is contained in:
tidusjar 2022-12-02 12:27:46 +00:00
parent a6274bc38d
commit cd6b70f771
15 changed files with 3540 additions and 440 deletions

View file

@ -43,8 +43,8 @@ jobs:
- name: Run Docker Image - name: Run Docker Image
run: nohup docker run --rm -p 5000:5000 ombi & run: nohup docker run --rm -p 5000:5000 ombi &
# - name: Run Wiremock Plex - name: Run Wiremock Plex
# run: nohup docker run -it --rm -p 8080:8080 --name wiremock wiremock/wiremock:2.35.0 run: nohup docker run -it --rm -p 32400:8080 --name wiremock wiremock/wiremock:2.35.0
- name: Sleep for server to start - name: Sleep for server to start
run: sleep 20 run: sleep 20

View file

@ -5,47 +5,47 @@
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Server Name</mat-label> <mat-label>Server Name</mat-label>
<input matInput placeholder="Server Name" name="name" [(ngModel)]="this.data.server.name" value="{{this.data.server.name}}"> <input matInput id="serverName" placeholder="Server Name" name="name" [(ngModel)]="this.data.server.name" value="{{this.data.server.name}}">
</mat-form-field> </mat-form-field>
<div class="row"> <div class="row">
<mat-form-field class="col-md-6 col-12" appearance="outline" floatLabel=auto> <mat-form-field class="col-md-6 col-12" appearance="outline" floatLabel=auto>
<mat-label>Hostname / IP</mat-label> <mat-label>Hostname / IP</mat-label>
<input matInput placeholder="Hostname or IP" name="ip" [(ngModel)]="this.data.server.ip" value="{{this.data.server.ip}}" <input matInput id="ip" placeholder="Hostname or IP" name="ip" [(ngModel)]="this.data.server.ip" value="{{this.data.server.ip}}"
#serverHostnameIpControl="ngModel" required> #serverHostnameIpControl="ngModel" required>
<mat-error *ngIf="serverHostnameIpControl.hasError('required')">Must be specified.</mat-error> <mat-error *ngIf="serverHostnameIpControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="col-md-4 col-7" appearance="outline" floatLabel=auto> <mat-form-field class="col-md-4 col-7" appearance="outline" floatLabel=auto>
<mat-label>Port</mat-label> <mat-label>Port</mat-label>
<input matInput placeholder="Port" name="port" [(ngModel)]="this.data.server.port" value="{{this.data.server.port}}" <input id="port" matInput placeholder="Port" name="port" [(ngModel)]="this.data.server.port" value="{{this.data.server.port}}"
#serverPortControl="ngModel" required pattern="^[0-9]*$"> #serverPortControl="ngModel" required pattern="^[0-9]*$">
<mat-error *ngIf="serverPortControl.hasError('required')">Must be specified.</mat-error> <mat-error *ngIf="serverPortControl.hasError('required')">Must be specified.</mat-error>
<mat-error *ngIf="serverPortControl.hasError('pattern')">Must be a number.</mat-error> <mat-error *ngIf="serverPortControl.hasError('pattern')">Must be a number.</mat-error>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="col-md-2 col-5 mt-3" id="ssl" name="ssl" [(ngModel)]="this.data.server.ssl" [checked]="this.data.server.ssl"> <mat-slide-toggle id="ssl" class="col-md-2 col-5 mt-3" id="ssl" name="ssl" [(ngModel)]="this.data.server.ssl" [checked]="this.data.server.ssl">
SSL SSL
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Plex Authorization Token</mat-label> <mat-label>Plex Authorization Token</mat-label>
<input matInput placeholder="Plex Authorization Token" name="authToken" [(ngModel)]="this.data.server.plexAuthToken" value="{{this.data.server.plexAuthToken}}" <input id="authToken" matInput placeholder="Plex Authorization Token" name="authToken" [(ngModel)]="this.data.server.plexAuthToken" value="{{this.data.server.plexAuthToken}}"
#serverApiKeyControl="ngModel" required> #serverApiKeyControl="ngModel" required>
<mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error> <mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Machine Identifier</mat-label> <mat-label>Machine Identifier</mat-label>
<input matInput placeholder="Machine Identifier" name="MachineIdentifier" [(ngModel)]="this.data.server.machineIdentifier" value="{{this.data.server.machineIdentifier}}" <input id="machineId" matInput placeholder="Machine Identifier" name="MachineIdentifier" [(ngModel)]="this.data.server.machineIdentifier" value="{{this.data.server.machineIdentifier}}"
#serverApiKeyControl="ngModel" required> #serverApiKeyControl="ngModel" required>
<mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error> <mat-error *ngIf="serverApiKeyControl.hasError('required')">Must be specified.</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Externally Facing Hostname</mat-label> <mat-label>Externally Facing Hostname</mat-label>
<input matInput placeholder="e.g. https://emby.this.data.server.com/" name="serverHostname" name="hostname" <input id="externalHostname" matInput placeholder="e.g. https://emby.this.data.server.com/" name="serverHostname" name="hostname"
[(ngModel)]="this.data.server.serverHostname" value="{{this.data.server.serverHostname}}" > [(ngModel)]="this.data.server.serverHostname" value="{{this.data.server.serverHostname}}" >
<mat-hint> <mat-hint>
This will be the external address that users will navigate to when they press the 'View On Plex' button This will be the external address that users will navigate to when they press the 'View On Plex' button
@ -58,7 +58,7 @@
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<mat-label>Episode Batch Size</mat-label> <mat-label>Episode Batch Size</mat-label>
<input matInput placeholder="150" name="MachineIdentifier" [(ngModel)]="this.data.server.episodeBatchSize" value="{{this.data.server.episodeBatchSize}}"> <input id="batchSize" matInput placeholder="150" name="MachineIdentifier" [(ngModel)]="this.data.server.episodeBatchSize" value="{{this.data.server.episodeBatchSize}}">
<mat-hint> <mat-hint>
150 by default, you shouldn't need to change this, this sets how many episodes we request from Plex at a single time. 150 by default, you shouldn't need to change this, this sets how many episodes we request from Plex at a single time.
</mat-hint> </mat-hint>
@ -66,7 +66,7 @@
<h2>Libraries</h2> <h2>Libraries</h2>
<div> <div>
<button mat-raised-button (click)="loadLibraries()" <button id="loadLibs" mat-raised-button (click)="loadLibraries()"
class="mat-focus-indicator mat-stroked-button mat-button-base">Load Libraries class="mat-focus-indicator mat-stroked-button mat-button-base">Load Libraries
<i class="fas fa-film"></i> <i class="fas fa-film"></i>
</button> </button>
@ -74,10 +74,10 @@
<div *ngIf="this.data.server.plexSelectedLibraries && this.data.server.plexSelectedLibraries.length > 0"> <div *ngIf="this.data.server.plexSelectedLibraries && this.data.server.plexSelectedLibraries.length > 0">
<label>Please select the libraries for Ombi to monitor. If nothing is selected, Ombi will monitor all <label>Please select the libraries for Ombi to monitor. If nothing is selected, Ombi will monitor all
libraries.</label> libraries.</label>
<div *ngFor="let lib of this.data.server.plexSelectedLibraries"> <div *ngFor="let lib of this.data.server.plexSelectedLibraries; let i = index">
<div class="md-form-field"> <div class="md-form-field">
<div class="checkbox"> <div class="checkbox">
<mat-slide-toggle [(ngModel)]="lib.enabled" [checked]="lib.enabled" <mat-slide-toggle [id]="lib[i]" [(ngModel)]="lib.enabled" [checked]="lib.enabled"
for="{{lib.title}}">{{lib.title}}</mat-slide-toggle> for="{{lib.title}}">{{lib.title}}</mat-slide-toggle>
</div> </div>
</div> </div>
@ -87,7 +87,7 @@
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align=end> <mat-dialog-actions align=end>
<button style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="accent" <button id="testPlexButton" style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="accent"
(click)="testPlex()"> (click)="testPlex()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;"> <span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-vial"></i> <i class="fas fa-vial"></i>
@ -95,7 +95,7 @@
</span> </span>
</button> </button>
<button style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="warn" <button id="deleteServer" style="margin: .5em 0 0 .5em;" align-middle mat-stroked-button color="warn"
(click)="delete()"> (click)="delete()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;"> <span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@ -103,7 +103,7 @@
</span> </span>
</button> </button>
<button style="margin: .5em 0 0 0.5em;" mat-stroked-button color="basic" (click)="cancel()"> <button id="cancel" style="margin: .5em 0 0 0.5em;" mat-stroked-button color="basic" (click)="cancel()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;"> <span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
<span> Cancel</span> <span> Cancel</span>
@ -111,7 +111,7 @@
</button> </button>
<button style="margin: .5em 0 0 .5em;" mat-stroked-button color="accent" <button id="saveServer" style="margin: .5em 0 0 .5em;" mat-stroked-button color="accent"
(click)="save()"> (click)="save()">
<span style="display: flex; align-items: baseline; white-space: pre-wrap;"> <span style="display: flex; align-items: baseline; white-space: pre-wrap;">
<i style="vertical-align: text-top;" class="fas fa-check"></i> <i style="vertical-align: text-top;" class="fas fa-check"></i>

View file

@ -25,13 +25,13 @@
<h2 style="margin: 1em 0 0 0;">Servers</h2> <h2 style="margin: 1em 0 0 0;">Servers</h2>
<mat-list style="display:flex; flex-flow: wrap;"> <mat-list style="display:flex; flex-flow: wrap;">
<mat-card class="server-card" *ngFor="let server of settings.servers"> <mat-card class="server-card" *ngFor="let server of settings.servers">
<button mat-button (click)="edit(server)"> <button mat-button (click)="edit(server)" id="{{server.name}}-button">
<h3>{{server.name}}</h3> <h3>{{server.name}}</h3>
</button> </button>
</mat-card> </mat-card>
<mat-card class="server-card new-server-card"> <mat-card class="server-card new-server-card">
<button mat-button (click)="newServer()"> <button mat-button (click)="newServer()" id="newServer">
<i class="fas fa-plus fa-xl"></i> <i class="fas fa-plus fa-xl"></i>
<h3>Manually Add Server</h3> <h3>Manually Add Server</h3>
</button> </button>
@ -109,13 +109,13 @@
<div class="md-form-field col-10"> <div class="md-form-field col-10">
<div *ngIf="!loadedServers"> <div *ngIf="!loadedServers">
<mat-form-field appearance="outline" floatLabel=auto> <mat-form-field appearance="outline" floatLabel=auto>
<input disabled matInput placeholder="No Servers Loaded" id="selectServer-noservers"> <input disabled matInput placeholder="No Servers Loaded" id="servers">
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="loadedServers"> <div *ngIf="loadedServers">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-select placeholder="Servers Loaded! Please Select"> <mat-select placeholder="Servers Loaded! Please Select" id="servers">
<mat-option (click)="selectServer(s)" <mat-option (click)="selectServer(s)"
*ngFor="let s of loadedServers.servers.server" [value]="s.server"> *ngFor="let s of loadedServers.servers.server" [value]="s.server">
{{s.name}}</mat-option> {{s.name}}</mat-option>

27
tests/cypress.config.ts Normal file
View file

@ -0,0 +1,27 @@
import { defineConfig } from 'cypress'
export default defineConfig({
watchForFileChanges: true,
chromeWebSecurity: false,
viewportWidth: 2560,
viewportHeight: 1440,
retries: {
runMode: 2,
openMode: 0,
},
env: {
username: 'a',
password: 'a',
},
projectId: 'o5451s',
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:5000',
specPattern: 'cypress/tests/**/*.spec.ts*',
excludeSpecPattern: ['**/snapshots/*'],
},
})

View file

@ -1,23 +0,0 @@
{
"$schema": "https://on.cypress.io/cypress.schema.json",
"supportFile": "cypress/support/index.ts",
"baseUrl": "http://localhost:5000",
"integrationFolder": "cypress/tests",
"testFiles": "**/*.spec.ts*",
"watchForFileChanges": true,
"chromeWebSecurity": false,
"viewportWidth": 2560,
"viewportHeight": 1440,
"retries": {
"runMode": 2,
"openMode": 0
},
"ignoreTestFiles": [
"**/snapshots/*"
],
"env": {
"username": "a",
"password": "a"
},
"projectId": "o5451s"
}

View file

@ -6,3 +6,5 @@ export * from './search/search.page';
export * from './user-preferences/user-preferences.page'; export * from './user-preferences/user-preferences.page';
export * from './requests/requests.page'; export * from './requests/requests.page';
export * from './details/movies/moviedetails.page'; export * from './details/movies/moviedetails.page';
export * from './settings/settings.page';
export * from './settings/plex/plex-settings.page';

View file

@ -0,0 +1,118 @@
import { BasePage } from "../../base.page";
class PlexCredentials {
get username(): Cypress.Chainable<any> {
return cy.get('#username');
}
get password(): Cypress.Chainable<any> {
return cy.get('#password');
}
get loadServers(): Cypress.Chainable<any> {
return cy.get('#loadServers');
}
get serverDropdown(): Cypress.Chainable<any> {
return cy.get('#servers');
}
}
class PlexServerModal {
get serverName(): Cypress.Chainable<any> {
return cy.get('#serverName');
}
get hostName(): Cypress.Chainable<any> {
return cy.get('#ip');
}
get port(): Cypress.Chainable<any> {
return cy.get('#port');
}
get ssl(): Cypress.Chainable<any> {
return cy.get('#ssl');
}
get authToken(): Cypress.Chainable<any> {
return cy.get('#authToken');
}
get machineIdentifier(): Cypress.Chainable<any> {
return cy.get('#machineId');
}
get externalHostname(): Cypress.Chainable<any> {
return cy.get('#externalHostname');
}
get batchSize(): Cypress.Chainable<any> {
return cy.get('#batchSize');
}
get loadLibraries(): Cypress.Chainable<any> {
return cy.get('#loadLibs');
}
get testButton(): Cypress.Chainable<any> {
return cy.get('#testPlexButton');
}
get deleteButton(): Cypress.Chainable<any> {
return cy.get('#deleteServer');
}
get cancelButton(): Cypress.Chainable<any> {
return cy.get('#cancel');
}
get saveButton(): Cypress.Chainable<any> {
return cy.get('#saveServer');
}
}
class PlexServersGrid {
serverCardButton(name: string): Cypress.Chainable<any> {
return cy.get(`#${name}-button`);
}
}
class PlexSettingsPage extends BasePage {
get enableCheckbox(): Cypress.Chainable<any> {
return cy.get('#enable');
}
get enableWatchlist(): Cypress.Chainable<any> {
return cy.get('#enableWatchlistImport');
}
get submit(): Cypress.Chainable<any> {
return cy.get('#save');
}
get fullySync(): Cypress.Chainable<any> {
return cy.get('#fullSync');
}
get partialSync(): Cypress.Chainable<any> {
return cy.get('#recentlyAddedSync');
}
get clearAndResync(): Cypress.Chainable<any> {
return cy.get('#clearData');
}
get runWatchlist(): Cypress.Chainable<any> {
return cy.get('#watchlistImport');
}
plexCredentials = new PlexCredentials();
plexServerModal = new PlexServerModal();
plexServerGrid = new PlexServersGrid();
constructor() {
super();
}
visit(options: Cypress.VisitOptions): Cypress.Chainable<Cypress.AUTWindow>;
visit(): Cypress.Chainable<Cypress.AUTWindow>;
visit(id: string): Cypress.Chainable<Cypress.AUTWindow>;
visit(id: string, options: Cypress.VisitOptions): Cypress.Chainable<Cypress.AUTWindow>;
visit(id?: any, options?: any) {
return cy.visit(`/Settings/Plex`, options);
}
}
export const plexSettingsPage = new PlexSettingsPage();

View file

@ -0,0 +1,20 @@
import { BasePage } from "../base.page";
class SettingsPage extends BasePage {
constructor() {
super();
}
visit(options: Cypress.VisitOptions): Cypress.Chainable<Cypress.AUTWindow>;
visit(): Cypress.Chainable<Cypress.AUTWindow>;
visit(id: string): Cypress.Chainable<Cypress.AUTWindow>;
visit(id: string, options: Cypress.VisitOptions): Cypress.Chainable<Cypress.AUTWindow>;
visit(id?: any, options?: any) {
return cy.visit(`/Settings/About`, options);
}
}
export const settingsPage = new SettingsPage();

View file

@ -12,10 +12,10 @@
// This function is called when a project is opened or re-opened (e.g. due to // This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing) // the project's config changing)
/** /**
* @type {Cypress.PluginConfig} * @type {Cypress.PluginConfig}
*/ */
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
} }

View file

@ -113,7 +113,7 @@ Cypress.Commands.add("getByData", (selector) => {
if (element.fireEvent) { if (element.fireEvent) {
element.fireEvent('on' + event); element.fireEvent('on' + event);
} else { } else {
var evObj = document.createEvent('Events'); const evObj = document.createEvent('Events');
evObj.initEvent(event, true, false); evObj.initEvent(event, true, false);

View file

@ -16,6 +16,7 @@
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import './commands'
import './request.commands'; import './request.commands';
import './plex-settings.commands';
import "cypress-real-events/support"; import "cypress-real-events/support";
import '@bahmutov/cy-api/support'; import '@bahmutov/cy-api/support';

View file

@ -0,0 +1,12 @@
Cypress.Commands.add('clearPlexServers', () => {
cy.request({
method: 'POST',
url: '/api/v1/Settings/Plex/',
body: `{"enable":false,"enableWatchlistImport":false,"monitorAll":false,"installId":"0c5c597d-56ea-4f34-8f59-18d34ec82482","servers":[],"id":2}`,
headers: {
'Authorization': 'Bearer ' + window.localStorage.getItem('id_token'),
'Content-Type':"application/json"
}
})
})

View file

@ -0,0 +1,122 @@
import { plexSettingsPage as Page } from "@/integration/page-objects";
describe("Plex Settings Tests", () => {
beforeEach(() => {
cy.login();
cy.clearPlexServers();
});
const plexTvApiResponse = `{
"success": true,
"message": null,
"servers": {
"server": [
{
"accessToken": "myaccessToken",
"name": "AutomationServer",
"address": "1.1.1.1",
"port": "32400",
"version": "1.30.0.6442-5070ad484",
"scheme": "http",
"host": "2.2.2.2",
"localAddresses": "localhost",
"machineIdentifier": "9999999999999999",
"createdAt": "5555555555",
"updatedAt": "6666666666",
"owned": "1",
"synced": "0",
"sourceTitle": null,
"ownerId": null,
"home": null
}
],
"friendlyName": "myPlex",
"identifier": "com.plexapp.plugins.myplex",
"machineIdentifier": "3dd86546546546540ff065465460c2654654654654",
"size": "1"
}
}
`;
it("Load Servers from Plex.TV Api and Save", () => {
loadServerFromPlexTvApi();
const modal = Page.plexServerModal;
modal.serverName.should('have.value','AutomationServer');
modal.hostName.should('have.value','localhost');
modal.port.should('have.value','32400');
modal.authToken.should('have.value','myaccessToken');
modal.machineIdentifier.should('have.value','9999999999999999');
modal.saveButton.click();
Page.plexServerGrid.serverCardButton('AutomationServer').should('be.visible');
Page.submit.click();
cy.wait("@plexSave");
});
it("Load Servers from Plex.TV Api and Edit", () => {
loadServerFromPlexTvApi();
const modal = Page.plexServerModal;
modal.saveButton.click();
Page.plexServerGrid.serverCardButton('AutomationServer').should('be.visible');
Page.submit.click();
cy.wait("@plexSave");
// Edit server
Page.plexServerGrid.serverCardButton('AutomationServer').click();
modal.serverName.should('have.value','AutomationServer');
modal.hostName.should('have.value','localhost');
modal.port.should('have.value','32400');
modal.authToken.should('have.value','myaccessToken');
modal.machineIdentifier.should('have.value','9999999999999999');
});
// Need to finish the witemock container
it.skip("Load Servers from Plex.TV Api and Test", () => {
loadServerFromPlexTvApi();
cy.intercept("POST", "api/v1/tester/plex", (req) => {
req.reply((res) => {
res.send(plexTvApiResponse);
});
}).as("testResponse");
const modal = Page.plexServerModal;
modal.testButton.click();
cy.wait("@testResponse");
});
function loadServerFromPlexTvApi() {
cy.intercept("POST", "api/v1/Plex/servers", (req) => {
req.reply((res) => {
res.send(plexTvApiResponse);
});
}).as("serverResponse");
cy.intercept("POST", "api/v1/Settings/Plex").as('plexSave');
Page.visit();
Page.plexCredentials.username.type('username');
Page.plexCredentials.password.type('password');
Page.plexCredentials.loadServers.click();
cy.wait("@serverResponse");
Page.plexCredentials.serverDropdown.click().get('mat-option').contains('AutomationServer').click();
}
});

View file

@ -1,7 +1,8 @@
{ {
"devDependencies": { "devDependencies": {
"@bahmutov/cy-api": "^1.5.0", "@bahmutov/cy-api": "^1.5.0",
"cypress": "6.8.0", "cypress": "11.2.0",
"cypress-cucumber-preprocessor": "^4.3.1",
"cypress-wait-until": "^1.7.1", "cypress-wait-until": "^1.7.1",
"typescript": "^4.2.3" "typescript": "^4.2.3"
}, },

File diff suppressed because it is too large Load diff