Merge pull request #217 from henrywhitaker3/dev

Merge dev into master
This commit is contained in:
Henry Whitaker
2020-08-02 20:28:28 +01:00
committed by GitHub
31 changed files with 865 additions and 243 deletions

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@ npm-debug.log
yarn-error.log
.vscode/
_ide_helper.php
.idea

View File

@@ -1,6 +1,6 @@
# Speedtest Tracker
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.7.8-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
[![Docker pulls](https://img.shields.io/docker/pulls/henrywhitaker3/speedtest-tracker?style=flat-square)](https://hub.docker.com/r/henrywhitaker3/speedtest-tracker) [![last_commit](https://img.shields.io/github/last-commit/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) [![issues](https://img.shields.io/github/issues/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/issues) [![commit_freq](https://img.shields.io/github/commit-activity/m/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/commits) ![version](https://img.shields.io/badge/version-v1.7.14-success?style=flat-square) [![license](https://img.shields.io/github/license/henrywhitaker3/Speedtest-Tracker?style=flat-square)](https://github.com/henrywhitaker3/Speedtest-Tracker/blob/master/LICENSE)
This program runs a speedtest check every hour and graphs the results. The back-end is written in [Laravel](https://laravel.com/) and the front-end uses [React](https://reactjs.org/). It uses the [Ookla's speedtest cli](https://www.speedtest.net/apps/cli) package to get the data and uses [Chart.js](https://www.chartjs.org/) to plot the results.

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ClearQueueCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'queue:clear';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear all queued jobs';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
DB::table('jobs')->delete();
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Helpers;
use App\Speedtest;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -236,15 +237,10 @@ class SpeedtestHelper {
$success = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', false)->get()[0]['rate'];
$fail = Speedtest::select(DB::raw('COUNT(id) as rate'))->whereDate('created_at', $day)->where('failed', true)->get()[0]['rate'];
if(( $success + $fail ) == 0) {
$percentage = 0;
} else {
$percentage = round(( $fail / ( $success + $fail ) * 100 ), 1);
}
array_push($rate, [
'date' => $day->toDateString(),
'rate' => $percentage
'success' => $success,
'failure' => $fail,
]);
}
@@ -253,4 +249,55 @@ class SpeedtestHelper {
return $rate;
}
/**
* Create a backup of the SQLite database
*
* @return boolean
*/
public static function dbBackup()
{
if(env('DB_CONNECTION') === 'sqlite') {
if(env('DB_DATABASE') !== null) {
$current = env('DB_DATABASE');
if(File::copy($current, $current . '.bak')) {
return true;
}
}
return false;
}
return null;
}
/**
* Delete all speedtests from the database
*
* @return boolean|string
*/
public static function deleteAll()
{
Cache::flush();
if(SpeedtestHelper::dbBackup() !== false) {
if(sizeof(Speedtest::whereNotNull('id')->get()) > 0) {
if(Speedtest::whereNotNull('id')->delete()) {
return [
'success' => true,
];
}
}
return [
'success' => true,
];
}
return [
'success' => false,
'msg' => 'There was an error backing up the database. No speedtests have been deleted.'
];
}
}

View File

@@ -147,4 +147,45 @@ class SpeedtestController extends Controller
], 500);
}
}
/**
* Delete all speedtests from db
*
* @return Response
*/
public function deleteAll()
{
$ret = SpeedtestHelper::deleteAll();
if($ret['success']) {
return response()->json([
'method' => 'delete all speedtests from the database',
'success' => true
], 200);
}
return response()->json([
'method' => 'delete all speedtests from the database',
'success' => false,
'error' => $ret['msg'],
], 500);
}
/**
* Delete a specific speedtest from the database
*
* @param Speedtest $speedtest
* @return boolean
*/
public function delete(Speedtest $speedtest)
{
$speedtest->delete();
Cache::flush();
return response()->json([
'method' => 'delete a speedtest from the database',
'success' => true,
], 200);
}
}

View File

@@ -44,7 +44,7 @@ class SpeedtestCompleteListener
}
}
if(SettingsHelper::get('telegram_bot_token') && SettingsHelper::get('telegram_chat_id')) {
if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) {
try {
config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]);
Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_chat_id')->value)

View File

@@ -31,7 +31,7 @@ class SpeedtestFailedListener
*/
public function handle($event)
{
if(SettingsHelper::get('slack_webhook')) {
if(SettingsHelper::get('slack_webhook')->value == true) {
try {
Notification::route('slack', SettingsHelper::get('slack_webhook')->value)
->notify(new SpeedtestFailedSlack());
@@ -41,7 +41,7 @@ class SpeedtestFailedListener
}
}
if(SettingsHelper::get('telegram_bot_token') && SettingsHelper::get('telegram_chat_id')) {
if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) {
try {
config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]);
Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_chat_id')->value)

View File

@@ -45,7 +45,7 @@ class SpeedtestOverviewListener
}
}
if(SettingsHelper::get('telegram_bot_token') && SettingsHelper::get('telegram_chat_id')) {
if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) {
try {
config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]);
Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_chat_id')->value)

View File

@@ -52,7 +52,7 @@ class TestNotificationListener
*/
private function slackNotification()
{
if(SettingsHelper::get('slack_webhook')) {
if(SettingsHelper::get('slack_webhook')->value == true) {
try {
Notification::route('slack', SettingsHelper::get('slack_webhook')->value)
->notify(new TestSlackNotification());
@@ -70,7 +70,7 @@ class TestNotificationListener
*/
private function telegramNotification()
{
if(SettingsHelper::get('telegram_bot_token') && SettingsHelper::get('telegram_chat_id')) {
if(SettingsHelper::get('telegram_bot_token')->value == true && SettingsHelper::get('telegram_chat_id')->value == true) {
try {
config([ 'services.telegram-bot-api' => [ 'token' => SettingsHelper::get('telegram_bot_token')->value ] ]);
Notification::route(TelegramChannel::class, SettingsHelper::get('telegram_bot_token')->value)

View File

@@ -1,4 +1,48 @@
{
"1.7.14": [
{
"description": "Updated dependencies.",
"link": ""
}
],
"1.7.13": [
{
"description": "Added command to clear application queue.",
"link": ""
}
],
"1.7.12": [
{
"description": "Updated dependencies.",
"link": ""
},
{
"description": "Added more unit tests.",
"link": ""
},
{
"description": "Display date on failure graph in local format.",
"link": ""
}
],
"1.7.11": [
{
"description": "Changed failure graph to a bar chart.",
"link": ""
}
],
"1.7.10": [
{
"description": "Fixed notifications bug.",
"link": ""
}
],
"1.7.9": [
{
"description": "Added ability to delete speedtests from the database.",
"link": ""
}
],
"1.7.8": [
{
"description": "Updated dependencies",

451
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ return [
|--------------------------------------------------------------------------
*/
'version' => '1.7.8',
'version' => '1.7.14',
/*
|--------------------------------------------------------------------------

1
database/.gitignore vendored
View File

@@ -1,2 +1,3 @@
*.sqlite
*.sqlite-journal
*.bak

22
package-lock.json generated
View File

@@ -3745,9 +3745,9 @@
"dev": true
},
"elliptic": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
"integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
@@ -8349,9 +8349,9 @@
}
},
"react-bootstrap": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.2.2.tgz",
"integrity": "sha512-G+QcEyBqFtakBNghdDugie+yU/ABDeqw3n+SOeRGxEn1m0dbIyHTroZpectcQk6FB3aS4RJGkZTuLVYH86Cu2A==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.3.0.tgz",
"integrity": "sha512-GYj0c6FO9mx7DaO8Xyz2zs0IcQ6CGCtM3O6/feIoCaG4N8B0+l4eqL7stlMcLpqO4d8NG2PoMO/AbUOD+MO7mg==",
"requires": {
"@babel/runtime": "^7.4.2",
"@restart/context": "^2.1.4",
@@ -8367,8 +8367,8 @@
"invariant": "^2.2.4",
"prop-types": "^15.7.2",
"prop-types-extra": "^1.1.0",
"react-overlays": "^4.0.0",
"react-transition-group": "^4.0.0",
"react-overlays": "^4.1.0",
"react-transition-group": "^4.4.1",
"uncontrollable": "^7.0.0",
"warning": "^4.0.3"
}
@@ -8405,9 +8405,9 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-overlays": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.0.0.tgz",
"integrity": "sha512-LpznWocwgeB5oWKg6cDdkqKP7MbX4ClKbJqgZGUMXPRBBYcqrgM6TjjZ/8DeurNU//GuqwQMjhmo/JVma4XEWw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.0.tgz",
"integrity": "sha512-vdRpnKe0ckWOOD9uWdqykLUPHLPndIiUV7XfEKsi5008xiyHCfL8bxsx4LbMrfnxW1LzRthLyfy50XYRFNQqqw==",
"requires": {
"@babel/runtime": "^7.4.5",
"@popperjs/core": "^2.0.0",

View File

@@ -28,7 +28,7 @@
"@babel/plugin-proposal-class-properties": "^7.10.4",
"chart.js": "^2.9.3",
"csv-file-validator": "^1.8.0",
"react-bootstrap": "^1.2.2",
"react-bootstrap": "^1.3.0",
"react-chartjs-2": "^2.9.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Axios from 'axios';
import { Spinner, Container, Row, Form, Card } from 'react-bootstrap';
import { Line } from 'react-chartjs-2';
import { Line, Bar } from 'react-chartjs-2';
import { Col } from 'react-bootstrap';
import { toast } from 'react-toastify';
@@ -61,6 +61,7 @@ export default class HistoryGraph extends Component {
};
var duOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} Mbit/s`,
@@ -100,6 +101,7 @@ export default class HistoryGraph extends Component {
};
var pingOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} ms`,
@@ -165,52 +167,45 @@ export default class HistoryGraph extends Component {
.then((resp) => {
var failData = {
labels: [],
datasets:[
datasets: [
{
data: [],
label: 'Failure',
borderColor: "#E74C3C",
fill: false,
label: 'Successful',
backgroundColor: '#07db71'
},
{
data: [],
label: 'Failed',
backgroundColor: '#E74C3C'
},
],
};
var failOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} %`,
label: (item) => `${item.yLabel} speedtests`,
},
},
title: {
display: false,
text: 'Ping results for the last ' + days + ' days',
},
scales: {
xAxes: [{
display: false,
scaleLabel: {
display: true,
labelString: 'DateTime'
}
stacked: true
}],
},
elements: {
point:{
radius: 0,
hitRadius: 8
}
yAxes: [{
stacked: true
}]
}
}
};
resp.data.data.forEach(e => {
var date = new Date(e.date);
var fail = {
t: date,
y: e.rate
};
failData.datasets[0].data.push(fail);
failData.labels.push(date.getFullYear() + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + '/' + ('0' + date.getDay()).slice(-2));
});
var success = {x: e.date, y: e.success};
var fail = {x: e.date, y: e.failure};
failData.datasets[0].data.push(success);
failData.datasets[1].data.push(fail);
failData.labels.push(new Date(e.date).toLocaleString([], {year: '2-digit', month:'2-digit', day:'2-digit'}));
})
console.log(failData);
this.setState({
failData: failData,
@@ -235,13 +230,8 @@ export default class HistoryGraph extends Component {
graph_failure_width: data.failure_graph_width.value,
});
if(this.state.graph_ul_dl_enabled || this.state.graph_ping_enabled) {
this.getDLULPing(days);
}
if(this.state.graph_failure_enabled) {
this.getFailure(days);
}
this.getDLULPing(days);
this.getFailure(days);
})
.catch((err) => {
console.log('Couldn\'t get the site config');
@@ -270,7 +260,29 @@ export default class HistoryGraph extends Component {
var pingData = this.state.pingData;
var pingOptions = this.state.pingOptions;
var failData = this.state.failData;
var failOptions = this.state.failOptions;
var failOptions = {
maintainAspectRatio: false,
responsive: true,
tooltips: {
callbacks: {
label: (item) => `${item.yLabel} speedtests`,
},
},
scales: {
xAxes: [{
stacked: true,
gridLines: {
display: false
}
}],
yAxes: [{
stacked: true,
ticks: {
stepSize: 1
}
}]
}
};
var days = this.state.days;
var graph_ul_dl_enabled = this.state.graph_ul_dl_enabled;
@@ -345,10 +357,10 @@ export default class HistoryGraph extends Component {
xs={{ span: 12 }}
className={failureClasses}
>
<Card className="shadow-sm">
<Card.Body>
<Line data={failData} options={pingOptions} height={440} />
</Card.Body>
<Card className="shadow-sm h-100">
<Card.Body className="w-100 h-100">
<Bar data={failData} options={failOptions} height={440} />
</Card.Body>
</Card>
</Col>
</Row>

View File

@@ -1,6 +1,8 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal } from 'react-bootstrap';
import { Modal, Button } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class TableRow extends Component {
constructor(props) {
@@ -25,6 +27,25 @@ export default class TableRow extends Component {
}
}
delete = (id) => {
var url = 'api/speedtest/delete/' + id;
Axios.delete(url)
.then((resp) => {
console.log(resp);
toast.success('Speedtest deleted');
})
.catch((err) => {
if(err.response.status == 404) {
toast.warning('Speedtest not found');
} else {
toast.error('Something went wrong');
}
})
this.toggleShow();
}
render() {
var e = this.state.data;
var show = this.state.show;
@@ -52,6 +73,7 @@ export default class TableRow extends Component {
{e.scheduled != undefined &&
<p>Type: {e.scheduled == true ? 'scheduled' : 'manual'}</p>
}
<Button variant="danger" onClick={() => { this.delete(e.id) }}>Delete</Button>
</Modal.Body>
</Modal>
</td>

View File

@@ -5,7 +5,7 @@ import LatestResults from '../Graphics/LatestResults';
import Footer from './Footer';
import DataRow from '../Data/DataRow';
import TestsTable from '../Graphics/TestsTable';
import Settings from './Settings';
import Settings from '../Settings/Settings';
export default class HomePage extends Component {

View File

@@ -0,0 +1,66 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Modal, Button } from 'react-bootstrap';
import SettingsModalCard from './SettingsModalCard';
import Axios from 'axios';
import { toast } from 'react-toastify';
export default class ResetSettings extends Component {
constructor(props) {
super(props)
this.state = {
show: false,
}
}
toggleShow = () => {
if(this.state.show) {
this.setState({ show: false });
} else {
this.setState({ show:true });
}
}
deleteAll = () => {
var url = 'api/speedtest/delete/all';
Axios.delete(url)
.then((resp) => {
toast.success('All speedtests have been deleted.');
this.toggleShow();
})
.catch((err) => {
if(err.response.data.error == undefined) {
toast.error('Something went wrong.');
}
toast.error(err.response.data.error);
})
}
render() {
var show = this.state.show;
const title = 'Reset Speedtests';
return (
<>
<SettingsModalCard title={title} description="Bulk delete speedtests from the database." toggleShow={this.toggleShow} />
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>Clear all speedtests</h4>
<p className="text-muted">If using SQLite, a backup of the database will be stored in the location of the current database.</p>
<Button onClick={this.deleteAll} variant="danger">Delete all</Button>
</Modal.Body>
</Modal>
</>
);
}
}
if (document.getElementById('ResetSettings')) {
ReactDOM.render(<ResetSettings />, document.getElementById('ResetSettings'));
}

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import { Card, Form, Button, Modal, Row, Col } from 'react-bootstrap';
import Axios from 'axios';
import { toast } from 'react-toastify';
import SettingsModalCard from '../Settings/SettingsModalCard';
export default class SettingWithModal extends Component {
constructor(props) {
@@ -98,15 +99,7 @@ export default class SettingWithModal extends Component {
return (
<>
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{title}</h4>
<p>{description}</p>
<Button variant="primary" onClick={this.toggleShow}>Edit</Button>
</div>
</Card.Body>
</Card>
<SettingsModalCard title={title} description={description} toggleShow={this.toggleShow} />
<Modal show={show} onHide={this.toggleShow}>
<Modal.Header>
<Modal.Title>{title}</Modal.Title>
@@ -130,7 +123,15 @@ export default class SettingWithModal extends Component {
readonly = true;
}
if(e.type == 'checkbox') {
if(e.type == 'info') {
return (
<Row key={e.obj.id} className="d-flex align-items-center">
<Col md={md} sm={sm}>
<p>{e.obj.content}</p>
</Col>
</Row>
)
} else if(e.type == 'checkbox') {
return (
<Row key={e.obj.id} className="d-flex align-items-center">
<Col md={md} sm={sm}>

View File

@@ -5,6 +5,7 @@ import Loader from '../Loader';
import Axios from 'axios';
import Setting from './Setting';
import SettingWithModal from './SettingWithModal';
import ResetSettings from './ResetSettings';
export default class Settings extends Component {
constructor(props) {
@@ -157,6 +158,9 @@ export default class Settings extends Component {
}
]} />
</Col>
<Col lg={{ span: 4 }} md={{ span: 6 }} sm={{ span: 12 }}>
<ResetSettings />
</Col>
</Row>
)
}

View File

@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Card, Button } from 'react-bootstrap';
export default class SettingsModalCard extends Component {
constructor(props) {
super(props)
this.state = {
title: this.props.title,
description: this.props.description,
toggleShow: this.props.toggleShow,
}
}
render() {
var title = this.state.title;
var description = this.state.description;
var toggleShow = this.state.toggleShow;
return (
<Card className="m-2 setting-card">
<Card.Body className="d-flex align-items-center">
<div>
<h4>{title}</h4>
<p>{description}</p>
<Button variant="primary" onClick={toggleShow}>Edit</Button>
</div>
</Card.Body>
</Card>
);
}
}
if (document.getElementById('SettingModalCard')) {
ReactDOM.render(<SettingsModalCard />, document.getElementById('SettingModalCard'));
}

View File

@@ -1,5 +1,6 @@
<?php
use App\Helpers\SpeedtestHelper;
use App\Http\Controllers\SpeedtestController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
@@ -29,6 +30,13 @@ Route::group([
->name('speedtest.fail');
Route::get('run', 'SpeedtestController@run')
->name('speedtest.run');
Route::group([
'prefix' => 'delete'
], function () {
Route::delete('all', 'SpeedtestController@deleteAll');
Route::delete('{speedtest}', 'SpeedtestController@delete');
});
});
Route::group([

View File

@@ -30,6 +30,6 @@ Route::get(SettingsHelper::getBase() . 'files/{path?}', function($file) {
->name('files');
Route::get('/{path?}', function() {
return view('app', [ 'title' => 'Speedtest Checker' ]);
return view('app', [ 'title' => 'Speedtest Tracker' ]);
})->where('path', '^((?!\/api\/).)*$')
->name('react');

View File

@@ -0,0 +1,21 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class AppVersionTest extends TestCase
{
/**
* Test the version CLI command
*
* @return void
*/
public function testVersionCommand()
{
$response = $this->artisan('speedtest:version')
->expectsOutput('Speedtest Tracker v' . config('speedtest.version'));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ConfigTest extends TestCase
{
use RefreshDatabase;
private $configStructure = [
'base',
'graphs' => [
'download_upload_graph_enabled' => [],
'download_upload_graph_width' => [],
'ping_graph_enabled' => [],
'ping_graph_width' => [],
'failure_graph_enabled' => [],
'failure_graph_width' => [],
],
'editable' => [
'slack_webhook',
'telegram_bot_token',
'telegram_chat_id'
],
];
/**
* Test config returned by API
*
* @return void
*/
public function testAPIConfig()
{
$response = $this->get('api/settings/config');
$response->assertJsonStructure($this->configStructure);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature;
use App\Rules\Cron;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class CronRuleTest extends TestCase
{
/**
* Test a valid CRON expression
*
* @return void
*/
public function testValidCronValidationRule()
{
$rule = [
'test' => new Cron,
];
$data = [
'test' => '*/5 * * * *',
];
$validator = $this->app['validator']->make($data, $rule);
$this->assertTrue($validator->passes());
}
/**
* Test an invalid CRON expression
*
* @return void
*/
public function testInvalidCronValidationRule()
{
$rule = [
'test' => new Cron,
];
$data = [
'test' => 'invalid',
];
$validator = $this->app['validator']->make($data, $rule);
$this->assertFalse($validator->passes());
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature;
use App\Helpers\SettingsHelper;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class SetSlackWebhookTest extends TestCase
{
use RefreshDatabase;
/**
* Test settings slack webhook via API
*
* @return void
*/
public function testSetSlackWebhookAPI()
{
$response = $this->json('PUT', 'api/settings', [
'name' => 'slack_webhook',
'value' => 'PHPUnitAPI'
]);
$response->assertStatus(200);
$this->assertEquals('PHPUnitAPI', SettingsHelper::get('slack_webhook')->value);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class SettingsTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function testExample()
{
$response = $this->get('/');
$response->assertStatus(200);
}
}