├── .DS_Store ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── changelog.md ├── composer.json ├── config └── api-tester.php ├── gulpfile.js ├── package.json ├── phpunit.xml ├── resources ├── assets │ ├── build │ │ ├── api-tester.css │ │ ├── api-tester.js │ │ └── img │ │ │ └── jsoneditor-icons.svg │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── js │ │ ├── api-tester.js │ │ ├── api-tester.vue │ │ ├── components │ │ │ ├── action-panel │ │ │ │ ├── action-panel.vue │ │ │ │ ├── request-actions.js │ │ │ │ └── request-type-select.vue │ │ │ ├── edit-block │ │ │ │ ├── edit-block.vue │ │ │ │ ├── request-editor-module.js │ │ │ │ ├── request-editor │ │ │ │ │ ├── headers │ │ │ │ │ │ ├── header.vue │ │ │ │ │ │ └── headers.vue │ │ │ │ │ ├── request-editor-data.js │ │ │ │ │ ├── request-editor.vue │ │ │ │ │ └── route-info.vue │ │ │ │ └── response-viewer │ │ │ │ │ ├── response-viewer-module.js │ │ │ │ │ └── response-viewer.vue │ │ │ ├── history │ │ │ │ ├── history-module.js │ │ │ │ ├── history-selector.vue │ │ │ │ └── moment.vue │ │ │ ├── json-editor │ │ │ │ ├── abstract-json-editor.vue │ │ │ │ ├── json-editor.vue │ │ │ │ └── json-viewer.vue │ │ │ ├── ligth-components │ │ │ │ ├── card-item.vue │ │ │ │ ├── card.vue │ │ │ │ ├── method-button.vue │ │ │ │ └── navigation-tabs.vue │ │ │ ├── lists-block │ │ │ │ └── lists-block.vue │ │ │ ├── requests │ │ │ │ ├── request.vue │ │ │ │ ├── requests-module.js │ │ │ │ └── requests-selector.vue │ │ │ ├── routes │ │ │ │ ├── route.vue │ │ │ │ ├── routes-module.js │ │ │ │ └── routes-selector.vue │ │ │ └── search │ │ │ │ ├── search-module.js │ │ │ │ └── search-panel.vue │ │ ├── plugins │ │ │ ├── api_demo │ │ │ │ ├── api-demo-installer.js │ │ │ │ ├── main.js │ │ │ │ └── src │ │ │ │ │ ├── Accessor.js │ │ │ │ │ ├── Api.js │ │ │ │ │ ├── JqueryPromiseProxy.js │ │ │ │ │ ├── components │ │ │ │ │ ├── api-monitor.vue │ │ │ │ │ └── warn-modal.vue │ │ │ │ │ └── vuex │ │ │ │ │ ├── api-actions.js │ │ │ │ │ └── api-module.js │ │ │ ├── api_demo_2 │ │ │ │ ├── api-demo-installer.js │ │ │ │ └── src │ │ │ │ │ ├── Api.js │ │ │ │ │ ├── JqueryPromiseProxy.js │ │ │ │ │ ├── ajax-manager.js │ │ │ │ │ └── components │ │ │ │ │ ├── api-monitor.vue │ │ │ │ │ └── warn-modal.vue │ │ │ ├── env.js │ │ │ └── globals.js │ │ └── vuex │ │ │ ├── actions.js │ │ │ ├── store.js │ │ │ └── vuex-installer.js │ └── sass │ │ ├── _jsoneditor.sass │ │ ├── _transitions.scss │ │ └── api-tester.sass └── views │ └── api-tester.blade.php ├── src ├── Collections │ ├── RequestCollection.php │ └── RouteCollection.php ├── Contracts │ ├── RequestRepositoryInterface.php │ ├── RouteRepositoryInterface.php │ └── StorageInterface.php ├── Entities │ ├── BaseEntity.php │ ├── RequestEntity.php │ └── RouteInfo.php ├── Exceptions │ ├── ApiTesterException.php │ └── FireBaseException.php ├── Http │ ├── Controllers │ │ ├── AssetsController.php │ │ ├── HomeController.php │ │ ├── RequestController.php │ │ └── RouteController.php │ ├── Middleware │ │ ├── DetectRoute.php │ │ └── PreventRedirect.php │ ├── Requests │ │ ├── StoreRequest.php │ │ └── UpdateRequest.php │ └── routes.php ├── Providers │ ├── RepositoryServiceProvider.php │ ├── RouteServiceProvider.php │ ├── StorageServiceProvide.php │ └── ViewServiceProvider.php ├── Repositories │ ├── RequestRepository.php │ ├── RouteDingoRepository.php │ ├── RouteLaravelRepository.php │ └── RouteRepository.php ├── ServiceProvider.php ├── Storages │ ├── FireBaseStorage.php │ └── JsonStorage.php └── View │ └── Composers │ └── ApiTesterComposer.php └── tests ├── JsonStorageTest.php ├── RequestCollectionTest.php ├── RouteCollectionTest.php └── TestCase.php /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | /vendor/ 4 | /composer.lock 5 | *.map 6 | /resources/assets/tmp/ 7 | 8 | /phpunit.bat 9 | /tests/tmp/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - 7.1 8 | - hhvm 9 | 10 | before_script: 11 | - travis_retry composer self-update 12 | 13 | install: 14 | - travis_retry composer install --no-interaction --prefer-dist -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 Yauheni Prakopchyk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Api Tester 2 | 3 | 4 | [![Unit tests](https://travis-ci.org/asvae/laravel-api-tester.svg?branch=master)](https://travis-ci.org/asvae/laravel-api-tester) 5 | [![PHP version](https://badge.fury.io/ph/asvae%2Flaravel-api-tester.svg)](https://badge.fury.io/ph/asvae%2Flaravel-api-tester) 6 | 7 | ![Interface](http://i.imgur.com/3geJtzb.png) 8 | 9 | ## Live demo 10 | Try it out: [laravel-api-tester.asva.by](http://laravel-api-tester.asva.by/) 11 | 12 | ## Docs 13 | Those are short and easy to read. Take a look. 14 | * [Interface](https://github.com/asvae/laravel-api-tester/wiki/Interface) 15 | * [FAQ](https://github.com/asvae/laravel-api-tester/wiki/Frequently-asked-questions) 16 | 17 | ## Installation 18 | 19 | Require this package with composer: 20 | 21 | ``` 22 | composer require asvae/laravel-api-tester 23 | ``` 24 | 25 | After updating composer, add the ServiceProvider to the providers array in config/app.php 26 | 27 | ``` 28 | Asvae\ApiTester\ServiceProvider::class, 29 | ``` 30 | 31 | That's it. Go to `[your site]/api-tester` and start testing routes. It works for Laravel 5.1+. 32 | 33 | ## Config 34 | 35 | By default, the package is bound to `APP_DEBUG` `.env` value. But you can easily override it. Just publish config: 36 | 37 | ``` 38 | php artisan vendor:publish --provider="Asvae\ApiTester\ServiceProvider" 39 | ``` 40 | 41 | And edit `config/api-tester.php` as you please. 42 | 43 | ## Features 44 | * Display routes for your application. 45 | * Prepare and save requests. 46 | * Collaborate with your team using firebase. 47 | * Live search for everything. 48 | * Filter out routes in [config](config/api-tester.php). 49 | * CSRF token is handled for you. 50 | * Fill request in JSON editor. 51 | * Preview response depending on type (html or json). 52 | * Clean and intuitive interface. 53 | * Lightweight and no dependencies (except on laravel). 54 | 55 | ## Powered By 56 | * [Vue.js](https://vuejs.org/) 57 | * [Bulma](http://bulma.io/) 58 | * [Json Editor](https://github.com/josdejong/jsoneditor) 59 | 60 | ## Feedback 61 | Don't hesitate to raise an issue if something doesn't work or you have a feature request. You're welcome to. 62 | 63 | ## Authors 64 | * [greabock](https://github.com/greabock) — backends. All of em. 65 | * [asvae](https://github.com/asvae) — frontends. You guessed it. 66 | 67 | ## Tests 68 | Check badges on the top for details. 69 | 70 | ## Licence 71 | MIT 72 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 (14.07.2016) Initial commit. 4 | ## 2.0.0 (23.08.2016) 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asvae/laravel-api-tester", 3 | "description": "Api tester for Laravel Framework", 4 | "keywords": ["laravel", "api", "debug"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Yauheni Prakopchyk", 9 | "email": "ontrew@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0" 14 | }, 15 | "require-dev": { 16 | "laravel/framework": "5.2.*", 17 | "phpunit/phpunit": "~4.0", 18 | "mockery/mockery": "^0.9.5" 19 | }, 20 | "suggest": { 21 | "ktamas77/firebase-php": "Keep your requests stored in cloud", 22 | "firebase/token-generator": "Token generator for firebase" 23 | }, 24 | "autoload-dev": { 25 | "classmap": [ 26 | "tests/TestCase.php" 27 | ] 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Asvae\\ApiTester\\": "src/" 32 | } 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Asvae\\ApiTester\\ServiceProvider" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/api-tester.php: -------------------------------------------------------------------------------- 1 | env('APP_DEBUG', false), 16 | 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default route 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Define the route for api router. 24 | | http://your-site.com/{route} 25 | | 26 | */ 27 | 28 | 'route' => 'api-tester', 29 | 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Middleware 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Define list of middleware, that should be used for api-tester. 37 | | This allows automatic CRSF token handling. 38 | | You can also use middleware groups, such as 'web' (Laravel 5.2+). 39 | | 40 | */ 41 | 42 | 'middleware' => [ 43 | Illuminate\Cookie\Middleware\EncryptCookies::class, 44 | Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 45 | Illuminate\Session\Middleware\StartSession::class, 46 | Illuminate\View\Middleware\ShareErrorsFromSession::class, 47 | ], 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Additional route meta information 52 | |-------------------------------------------------------------------------- 53 | | Displays additional route information. Such as request rules and comments. 54 | | 55 | | !WARNING! 56 | | This sometimes causes fatal errors, rendering api tester unusable. 57 | | Set to false if that's your case. 58 | */ 59 | 60 | 'route_meta' => true, 61 | 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Request analysis 66 | |-------------------------------------------------------------------------- 67 | | Display request rules.. 68 | | 69 | | !WARNING! 70 | | This sometimes causes fatal errors, rendering api tester unusable. 71 | | Set to false if that's your case. 72 | */ 73 | 74 | 'request_rules' => true, 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Filter routes 79 | |-------------------------------------------------------------------------- 80 | | All your routes will be filtered via given patterns. Both include and 81 | | exclude are always applied. You can also use regex when needed. 82 | | 83 | | ## Examples 84 | | 85 | | ### Include all 86 | | 'include' => [] 87 | | 88 | | ### Include some routes 89 | | 'include' => [ 90 | | 'api/users', 91 | | 'api/sales', 92 | | // ... 93 | | ] 94 | | 95 | | ### Include/exclude advanced syntax 96 | | 'include' => [ 97 | | [ 98 | | 'path' => 'api/v(1|2|3)/.*', 99 | | 'name' => '.*', 100 | | 'method' => '(GET|POST|PUT|PATCH|DELETE)' 101 | | ], 102 | | // ... 103 | | ] 104 | | 105 | | ### Include all except 'api/users' 106 | | 'include' => [], 107 | | 'exclude' => ['api/users'], 108 | | 109 | */ 110 | 111 | 'include' => '.*', 112 | 'exclude' => [ 113 | 'api-tester', 114 | ], 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Repositories 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Specify list of Route Repositories that to be used for providing routes. 122 | | 123 | */ 124 | 125 | 'route_repositories' => [ 126 | Asvae\ApiTester\Repositories\RouteLaravelRepository::class, 127 | //Asvae\ApiTester\Repositories\RouteDingoRepository::class, 128 | ], 129 | 130 | /* 131 | |-------------------------------------------------------------------------- 132 | | Request Repository 133 | |-------------------------------------------------------------------------- 134 | | Define class of request repository. 135 | | 136 | */ 137 | 138 | 'request_repository' => Asvae\ApiTester\Repositories\RequestRepository::class, 139 | 140 | /* 141 | |-------------------------------------------------------------------------- 142 | | Asvae\ApiTester\Repositories\RequestRepository configuration 143 | |-------------------------------------------------------------------------- 144 | | This config matters only when using Asvae\ApiTester\Repositories\RequestRepository 145 | | or similar implementations. 146 | | 147 | */ 148 | 149 | 'storage_driver' => 'file', 150 | 151 | 'storage_drivers' => [ 152 | 'file' => [ 153 | 'class' => Asvae\ApiTester\Storages\JsonStorage::class, 154 | 'options' => [ 155 | 'path' => 'storage/api-tester/requests.db' 156 | ] 157 | ], 158 | 'firebase' => [ 159 | 'class' => Asvae\ApiTester\Storages\FireBaseStorage::class, 160 | 'options' => [ 161 | 'base' => env('API_TESTER_FIREBASE_ADDRESS', 'https://example.firebaseio.com/api-tester/'), 162 | ], 163 | 'token' => [ 164 | 'secret' => env('API_TESTER_FIREBASE_SECRET', ''), 165 | 'options' => ['admin' => true], 166 | 'data' => [], 167 | ] 168 | ] 169 | ] 170 | ]; 171 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var elixir = require('laravel-elixir') 2 | 3 | var paths = { 4 | js: [], 5 | css: [], 6 | } 7 | 8 | elixir(function (mix) { 9 | mix.sass('api-tester.sass', './resources/assets/tmp') 10 | paths.css.push('./resources/assets/tmp/api-tester.css') 11 | 12 | mix.browserify('api-tester.js', './resources/assets/tmp/app.js') 13 | paths.js.push('./resources/assets/tmp/app.js') 14 | 15 | mix.copy('./node_modules/font-awesome/fonts/**', 'resources/assets/fonts') 16 | 17 | mix.scripts(paths.js, './resources/assets/build/api-tester.js', './') 18 | mix.styles(paths.css, './resources/assets/build/api-tester.css', './') 19 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "prod": "gulp --production", 5 | "dev": "gulp watch" 6 | }, 7 | "devDependencies": { 8 | "babel-plugin-transform-runtime": "^6.7.5", 9 | "babel-preset-stage-2": "^6.11.0", 10 | "browserify": "^13.0.1", 11 | "bulma": "^0.1.0", 12 | "font-awesome": "^4.6.1", 13 | "gulp": "^3.9.1", 14 | "jquery": "^3.0.0", 15 | "jquery-mask-plugin": "^1.14.0", 16 | "jsoneditor": "^5.5.6", 17 | "laravel-elixir": "^6.0.0-1", 18 | "laravel-elixir-browserify-official": "^0.1.2", 19 | "laravel-elixir-browsersync-official": "^1.0.0", 20 | "lockr": "^0.8.4", 21 | "lodash": "^4.11.1", 22 | "moment": "^2.14.1", 23 | "randexp": "^0.4.3", 24 | "vue": "^1.0.21", 25 | "vue-hot-reload-api": "^2.0.5", 26 | "vue-resource": "^0.9.3", 27 | "vue-strap": "^1.0.7", 28 | "vueify": "^8.3.9", 29 | "vueify-insert-css": "^1.0.0", 30 | "vuex": "^1.0.0-rc.2" 31 | }, 32 | "browserify": { 33 | "transform": [ 34 | [ 35 | "babelify", 36 | { 37 | "presets": [ 38 | "es2015", 39 | "stage-2" 40 | ] 41 | } 42 | ], 43 | [ 44 | "vueify" 45 | ] 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | 18 | src/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/assets/build/img/jsoneditor-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | JSON Editor Icons 4 | image/svg+xmlJSON Editor Icons 5 | 6 | 7 | background 8 | 9 | 10 | 11 | Layer 1 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /resources/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/resources/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /resources/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/resources/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /resources/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/resources/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /resources/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/resources/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /resources/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asvae/laravel-api-tester/7883a4ef81eb1610d327f968ebe5848face9157d/resources/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /resources/assets/js/api-tester.js: -------------------------------------------------------------------------------- 1 | import './plugins/env.js' 2 | import './plugins/globals.js' 3 | import './vuex/vuex-installer.js' 4 | import './plugins/api_demo/api-demo-installer.js' 5 | import './plugins/api_demo_2/api-demo-installer.js' 6 | 7 | import Vue from 'vue' 8 | import vmApiTesterMain from './api-tester.vue' 9 | import store from './vuex/store.js' 10 | 11 | new Vue({ 12 | store, 13 | el: '#api-tester', 14 | components: { 15 | vmApiTesterMain 16 | }, 17 | }) 18 | 19 | -------------------------------------------------------------------------------- /resources/assets/js/api-tester.vue: -------------------------------------------------------------------------------- 1 | Application main page. 2 | 3 | 59 | 60 | 73 | 74 | We decided to forsake all the mobile support stuff. It requires bulma @media 75 | which requires scss in .vue file, which is not supported by phpstorm ATM. 76 | 123 | 124 | -------------------------------------------------------------------------------- /resources/assets/js/components/action-panel/action-panel.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 98 | 99 | -------------------------------------------------------------------------------- /resources/assets/js/components/action-panel/request-actions.js: -------------------------------------------------------------------------------- 1 | export const saveRequest = function ({dispatch}, request, next = () => {}) { 2 | this.$api_demo2.load({url: 'requests/store'}, request) 3 | .then(function (data) { 4 | dispatch('SET_CURRENT_REQUEST', data.data) 5 | next() 6 | }) 7 | } 8 | 9 | export const updateRequest = function ({dispatch}, request, next = () => {}) { 10 | this.$api_demo2.load({url: 'requests/update'}, request) 11 | .then(function (response) { 12 | dispatch('SET_CURRENT_REQUEST', response.data) 13 | dispatch('UPDATE_REQUEST', response.data) 14 | next() 15 | }) 16 | } -------------------------------------------------------------------------------- /resources/assets/js/components/action-panel/request-type-select.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/edit-block.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 174 | 175 | 178 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor-module.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | mode: 'data', 3 | isSending: false, 4 | scheduledList: [], 5 | } 6 | 7 | const mutations = { 8 | SET_EDITOR_MODE(state, mode){ 9 | state.mode = mode 10 | }, 11 | SET_REQUEST_IS_SENDING(state, value = true){ 12 | state.isSending = value 13 | }, 14 | SCHEDULE_REQUEST(state, request){ 15 | state.scheduledList.push(request) 16 | }, 17 | SHIFT_REQUEST(state){ 18 | state.scheduledList.shift() 19 | }, 20 | } 21 | 22 | export default {state, mutations,} 23 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor/headers/header.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 55 | 56 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor/headers/headers.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 47 | 48 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor/request-editor-data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | request: {'path': '/', 'name': '', 'method': 'GET', 'headers': [], }, 3 | } -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor/request-editor.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 76 | 77 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/request-editor/route-info.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 131 | 132 | 158 | -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/response-viewer/response-viewer-module.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | mode: 'data', 3 | response: null, 4 | } 5 | 6 | const mutations = { 7 | SET_RESPONSE(state, response){ 8 | state.response = response 9 | }, 10 | SET_VIEWER_MODE(state, mode){ 11 | state.mode = mode 12 | }, 13 | } 14 | 15 | export default {state, mutations} -------------------------------------------------------------------------------- /resources/assets/js/components/edit-block/response-viewer/response-viewer.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 76 | 77 | -------------------------------------------------------------------------------- /resources/assets/js/components/history/history-module.js: -------------------------------------------------------------------------------- 1 | // History if loaded from local storage. 2 | let history = window.localStorage.getItem('api-tester.history') 3 | try { 4 | history = JSON.parse(history) 5 | if (history === null) history = [] 6 | } catch (e) { 7 | history = [] 8 | } 9 | 10 | const state = { 11 | history, 12 | } 13 | 14 | const mutations = { 15 | SET_HISTORY(state, moment){ 16 | state.history.push({ 17 | method: moment.method, 18 | path : moment.path, 19 | body : moment.body, 20 | headers: moment.headers, 21 | createdAt: new Date().getTime(), 22 | }) 23 | window.localStorage.setItem('api-tester.history', JSON.stringify(history)) 24 | }, 25 | CLEAR_HISTORY(state){ 26 | window.localStorage.setItem('api-tester.history', '') 27 | state.history = [] 28 | }, 29 | } 30 | 31 | export default {state, mutations} 32 | -------------------------------------------------------------------------------- /resources/assets/js/components/history/history-selector.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | 74 | 75 | 80 | -------------------------------------------------------------------------------- /resources/assets/js/components/history/moment.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 67 | 68 | -------------------------------------------------------------------------------- /resources/assets/js/components/json-editor/abstract-json-editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /resources/assets/js/components/json-editor/json-editor.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /resources/assets/js/components/json-editor/json-viewer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /resources/assets/js/components/ligth-components/card-item.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /resources/assets/js/components/ligth-components/card.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 39 | 40 | -------------------------------------------------------------------------------- /resources/assets/js/components/ligth-components/method-button.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /resources/assets/js/components/ligth-components/navigation-tabs.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | 34 | -------------------------------------------------------------------------------- /resources/assets/js/components/lists-block/lists-block.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | 37 | -------------------------------------------------------------------------------- /resources/assets/js/components/requests/request.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 75 | 76 | -------------------------------------------------------------------------------- /resources/assets/js/components/requests/requests-module.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const state = { 4 | requests: [], 5 | currentRequest: {method: 'GET', path: '/', headers: [], body: ''}, 6 | } 7 | 8 | const mutations = { 9 | SET_CURRENT_REQUEST(state, currentRequest){ 10 | state.currentRequest = currentRequest 11 | }, 12 | SET_REQUESTS(state, requests){ 13 | state.requests = requests 14 | }, 15 | INSERT_REQUEST: (state, request) => { 16 | let requests = _.cloneDeep(state.requests) 17 | requests.push(request) 18 | state.requests = requests 19 | }, 20 | DELETE_REQUEST: (state, request) => { 21 | let requests = _.cloneDeep(state.requests) 22 | let index = _.findIndex(requests, request); 23 | if(index !== -1) { 24 | requests.splice(index, 1) 25 | state.requests = requests 26 | } 27 | }, 28 | UPDATE_REQUEST: (state, request) => { 29 | let requests = _.cloneDeep(state.requests) 30 | let index = _.findIndex(requests, {id: request.id}); 31 | if(index !== -1){ 32 | requests.splice(index, 1, request) 33 | state.requests = requests 34 | } 35 | }, 36 | } 37 | 38 | export default {state, mutations} 39 | -------------------------------------------------------------------------------- /resources/assets/js/components/requests/requests-selector.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 136 | 137 | 142 | -------------------------------------------------------------------------------- /resources/assets/js/components/routes/route.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 80 | 81 | -------------------------------------------------------------------------------- /resources/assets/js/components/routes/routes-module.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const state = { 4 | errorLoading: false, // Can't load routes 5 | infoError: false, // Can't retrieve route info 6 | routes: [], 7 | currentRoute: null, 8 | isLoading: false, 9 | } 10 | 11 | const mutations = { 12 | SET_ROUTES: (state, routes) => { 13 | state.routes = routes 14 | }, 15 | SET_ROUTES_ERROR(state, result){ 16 | state.errorLoading = result 17 | }, 18 | SET_INFO_ERROR: (state, error) => { 19 | state.infoError = error 20 | }, 21 | SET_REQUEST_INFO: (state, route) => { 22 | if (route === null) { 23 | state.currentRoute = route 24 | return 25 | } 26 | state.currentRoute = _.find(state.routes, route) 27 | }, 28 | SET_ROUTES_LOADING: (sate, isLoading) => { 29 | state.isLoading = isLoading 30 | } 31 | } 32 | 33 | export default {state, mutations} 34 | -------------------------------------------------------------------------------- /resources/assets/js/components/routes/routes-selector.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 85 | 86 | 96 | -------------------------------------------------------------------------------- /resources/assets/js/components/search/search-module.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | search: '', 3 | } 4 | 5 | const mutations = { 6 | SET_SEARCH: (state, search) => { 7 | state.search = search 8 | }, 9 | } 10 | 11 | export default {state, mutations} 12 | -------------------------------------------------------------------------------- /resources/assets/js/components/search/search-panel.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | 51 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/api-demo-installer.js: -------------------------------------------------------------------------------- 1 | // NOTE The library is in early development stage. Beware. 2 | 3 | import Vue from 'vue' 4 | import ApiDemo from './main.js' 5 | 6 | Vue.use(ApiDemo) -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/main.js: -------------------------------------------------------------------------------- 1 | import Api from './src/Api.js' 2 | export default function (Vue, options) { 3 | Object.defineProperties(Vue.prototype, { 4 | $api: { 5 | get () { 6 | return new Api(this, options) 7 | } 8 | }, 9 | }) 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/Accessor.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import _ from 'lodash' 3 | import store from '../../../vuex/store.js' 4 | import * as actions from './vuex/api-actions' 5 | 6 | export default new Vue({ 7 | data (){ 8 | return { 9 | syncRoutes: [], 10 | warnRoutes: {}, 11 | } 12 | }, 13 | vuex: { 14 | getters: { 15 | urls: state => state.api.activeUrls 16 | }, 17 | actions 18 | }, 19 | methods: { 20 | isActive(url){ 21 | // Might be undefined so we convert to boolean 22 | return this.urls[url] ? true : false 23 | }, 24 | warn(route, confirm){ 25 | let warn = { 26 | route, 27 | message: this.warnRoutes[route], 28 | confirm, 29 | } 30 | this.addUrlToWarn(warn) 31 | }, 32 | requiresWarn(url){ 33 | return this.warnRoutes[url] ? true : false 34 | }, 35 | isSync(url){ 36 | return _.includes(this.syncRoutes, url) 37 | }, 38 | getEmptyPromise() { 39 | return { 40 | then: () => this, 41 | catch: () => this, 42 | } 43 | } 44 | }, 45 | store 46 | }) -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/Api.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import $ from 'jquery' 3 | import accessor from './Accessor.js' 4 | import JqueryPromiseProxy from './JqueryPromiseProxy' 5 | 6 | // Double click protected routes. 7 | accessor.syncRoutes = [ 8 | // TODO remove to config file 9 | 'some/route', 10 | ] 11 | 12 | // Show notification for following routes. 13 | accessor.warnRoutes = { 14 | // TODO remove to config file 15 | 'some/route': 'Message to show', 16 | } 17 | 18 | export default class Api { 19 | constructor(vm, options) { 20 | this._vm = vm 21 | this._options = _.isObject(options) ? options : {} 22 | } 23 | 24 | /** 25 | * @param route string 26 | * @param data object 27 | * @returns {*} 28 | */ 29 | load(route, data) { 30 | 31 | // Double click protection. 32 | if (accessor.isActive(route) && accessor.isSync(route)) { 33 | console.warn('You\'re clicking too fast...') 34 | return accessor.getEmptyPromise() 35 | } 36 | 37 | // Confirmation dialogue. 38 | if (accessor.requiresWarn(route)) { 39 | let jqueryPromiseProxy = new JqueryPromiseProxy 40 | let promise = new Promise(function (resolve, reject) { 41 | accessor.warn(route, resolve) 42 | }) 43 | 44 | promise.then(function () { 45 | let jqueryPromise = this._jqueryAjax(route, data) 46 | // If request was confirmed, we'll apply all thens and 47 | // catches from proxy to our new jquery request. 48 | jqueryPromiseProxy.applyToJqueryPromise(jqueryPromise) 49 | }.bind(this)) 50 | 51 | return jqueryPromiseProxy 52 | } 53 | 54 | return this._jqueryAjax(route, data) 55 | } 56 | 57 | getCookie(name) { 58 | var matches = document.cookie.match(new RegExp( 59 | "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" 60 | )) 61 | return matches ? decodeURIComponent(matches[1]) : undefined 62 | } 63 | 64 | /** 65 | * Make ajax request 66 | * 67 | * @param method 68 | * @param url 69 | * @param data 70 | * @param headers 71 | * @returns {*} 72 | */ 73 | ajax(method, url, data = null, headers = []) { 74 | headers = _.clone(headers) 75 | if(!_.some(['GET', 'HEAD', 'OPTIONS'], method.toUpperCase())){ 76 | headers.push({key: 'X-XSRF-TOKEN', value: this.getCookie('XSRF-TOKEN') }) 77 | } else { 78 | if(data === null){ 79 | data = {} 80 | } 81 | data.__noCache = Date.now() 82 | } 83 | 84 | headers = _.reduce(headers, function (headersHash, header) { 85 | headersHash[header.key] = header.value 86 | return headersHash 87 | }, {}) 88 | 89 | if (method.toUpperCase() !== 'GET') { 90 | data = JSON.stringify(data) 91 | } 92 | 93 | return $.ajax({ 94 | url, 95 | data, 96 | method, 97 | headers, 98 | dataType: 'json', 99 | contentType: 'application/json; charset=utf-8', 100 | context: this._vm, 101 | }) 102 | } 103 | 104 | 105 | /** 106 | * Make simplified ajax request for route. 107 | * 108 | * @param route 109 | * @param data 110 | * @returns {*} 111 | */ 112 | _jqueryAjax(route, data) { 113 | 114 | // Check request started. 115 | accessor.activateUrl(route) 116 | 117 | return $.ajax({ 118 | url: route, 119 | data: JSON.stringify(data), 120 | method: 'POST', 121 | headers: { 122 | 'X-CSRF-TOKEN': ENV.token 123 | }, 124 | dataType: 'json', 125 | contentType: 'application/json; charset=utf-8', 126 | context: this._vm, 127 | }) 128 | .done(function (response) { 129 | }) 130 | .fail(function (response) { 131 | }) 132 | .always(function () { 133 | // Check request finished. 134 | accessor.deactivateUrl(route) 135 | }) 136 | } 137 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/JqueryPromiseProxy.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | /** 4 | * This class works as Jquery proxy, meaning the application 5 | * won't notice any difference when you use it instead of original one. 6 | */ 7 | export default class JqueryPromiseProxy { 8 | 9 | constructor() { 10 | this._thens = [] 11 | this._catches = [] 12 | } 13 | 14 | then(callback) { 15 | this._thens.push(callback) 16 | } 17 | 18 | catch(callback) { 19 | this._catches.push(callback) 20 | } 21 | 22 | applyToJqueryPromise(promise) { 23 | _.each(this._thens, function (callback) { 24 | promise.then(callback) 25 | }) 26 | _.each(this._catches, function (callback) { 27 | promise.catch(callback) 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/components/api-monitor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/components/warn-modal.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 77 | 78 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/vuex/api-actions.js: -------------------------------------------------------------------------------- 1 | export const activateUrl = function ({dispatch}, url) { 2 | dispatch('SET_ACTIVE_URL', url, true) 3 | } 4 | 5 | export const deactivateUrl = function ({dispatch}, url) { 6 | dispatch('SET_ACTIVE_URL', url, undefined) 7 | } 8 | 9 | export const addUrlToWarn = function ({dispatch}, warn) { 10 | dispatch('ADD_WARN', warn) 11 | } 12 | 13 | export const removeUrlToWarn = function ({dispatch}, warn) { 14 | dispatch('REMOVE_WARN', warn) 15 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo/src/vuex/api-module.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const state = { 4 | activeUrls: {}, 5 | urlsToWarn: [], 6 | } 7 | 8 | const mutations = { 9 | SET_ACTIVE_URL(state, url, value){ 10 | // Cloning is required to trigger watcher. 11 | state.activeUrls[url] = value 12 | state.activeUrls = _.clone(state.activeUrls) 13 | }, 14 | ADD_WARN(state, warn){ 15 | state.urlsToWarn.push(warn) 16 | }, 17 | REMOVE_WARN(state, warn){ 18 | state.urlsToWarn.$remove(warn) 19 | } 20 | } 21 | 22 | export default { 23 | state, 24 | mutations 25 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/api-demo-installer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import accessor from './src/ajax-manager.js' 3 | import Api from './src/Api.js' 4 | 5 | Vue.use(function (Vue, options) { 6 | Object.defineProperties(Vue.prototype, { 7 | $api_demo2: { 8 | get () { 9 | return new Api(this, options) 10 | } 11 | }, 12 | $activeActions: { 13 | get: () => accessor.activeUrls 14 | }, 15 | $accessor: { 16 | get: () => accessor 17 | }, 18 | }) 19 | }) -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/src/Api.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import $ from 'jquery' 3 | import accessor from './ajax-manager.js' 4 | import JqueryPromiseProxy from './JqueryPromiseProxy' 5 | 6 | // Busy route will discard new requests. 7 | accessor.syncRoutes = [] 8 | 9 | // Route needs warning 10 | accessor.warnRoutes = { 11 | 'cashier/sales/delete': 'Message', 12 | } 13 | 14 | // This creates a response query so that only one could be running at 15 | // any given moment, yet the last scheduled response will always run. 16 | // Useful for live search and such. When race condition is inappropriate. 17 | accessor.hold = [ 18 | 'boards/index' 19 | ] 20 | 21 | export default class Api { 22 | constructor(vm, options) { 23 | this._vm = vm 24 | this._options = _.isObject(options) ? options : {} 25 | } 26 | 27 | /** 28 | * @param options string|object 29 | * @param data object 30 | * @returns {*} 31 | */ 32 | load(options, data) { 33 | 34 | let route = typeof options === 'string' ? options : options.url 35 | 36 | // Response for busy route will be discarded. 37 | if (accessor.isActive(route) && accessor.isSync(route)) { 38 | return accessor.getEmptyPromise() 39 | } 40 | 41 | // Request requires notification. 42 | if (accessor.requiresWarn(route)) { 43 | let jqueryPromiseProxy = new JqueryPromiseProxy 44 | let promise = new Promise(function (resolve, reject) { 45 | accessor.warn(route, resolve) 46 | }) 47 | 48 | promise.then(function () { 49 | let jqueryPromise = this.jqueryAjax(options, data) 50 | // Load real jquery promise with all thens and catches. 51 | jqueryPromiseProxy.applyToJqueryPromise(jqueryPromise) 52 | }.bind(this)) 53 | 54 | return jqueryPromiseProxy 55 | } 56 | 57 | // Next response always waits for previous to finish. 58 | // Only last next response will be sent. 59 | if (accessor.requiresHold(route)) { 60 | if (accessor.isActive(route)) { 61 | let jqueryPromiseProxy = new JqueryPromiseProxy 62 | // If route is busy we'll store the response and return 63 | // promise proxy. 64 | accessor.holdenRoutes[route] = () => { 65 | let jqueryPromise = this.load(options, data) 66 | jqueryPromiseProxy.applyToJqueryPromise(jqueryPromise) 67 | } 68 | 69 | return jqueryPromiseProxy 70 | } else { 71 | // If route is free we'll send it right away. 72 | // Also we'll bind stored request on always. 73 | accessor.holdenRoutes[route] = () => { 74 | } 75 | let jqueryPromise = this.jqueryAjax(options, data) 76 | jqueryPromise.always(() => { 77 | accessor.holdenRoutes[route]() 78 | accessor.holdenRoutes[route] = () => { 79 | } 80 | }) 81 | return jqueryPromise 82 | } 83 | } 84 | 85 | return this.jqueryAjax(options, data) 86 | } 87 | 88 | /** 89 | * Call ajax to route. 90 | * 91 | * @param route 92 | * @param data 93 | * @returns {*} 94 | */ 95 | jqueryAjax(options, data) { 96 | 97 | let route = typeof options === 'string' ? options : options.url 98 | let jqueryOptions = typeof options === 'string' ? {} : options 99 | 100 | let defaultOptions = { 101 | url: '/api/' + route, 102 | data: JSON.stringify(data), 103 | method: 'POST', 104 | headers: { 105 | 'X-CSRF-TOKEN': ENV.token 106 | }, 107 | dataType: 'json', 108 | contentType: 'application/json; charset=utf-8', 109 | context: this._vm, 110 | } 111 | 112 | // Merge options with defaults. 113 | jqueryOptions = _.defaults(jqueryOptions, defaultOptions) 114 | 115 | // This block handles detection of request start and end. 116 | let wasUndefined = accessor.activeUrls[route] === undefined 117 | // Mark the request start 118 | accessor.activeUrls[route] = true 119 | // Gonna clone the tree if route is called first time. 120 | if (wasUndefined) { 121 | accessor.activeUrls = _.clone(accessor.activeUrls) 122 | } 123 | 124 | return $.ajax(jqueryOptions) 125 | .done(function () { 126 | 127 | }) 128 | .fail(function () { 129 | 130 | }) 131 | .always(function () { 132 | // Mark request ended. 133 | accessor.activeUrls[route] = false 134 | }) 135 | 136 | } 137 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/src/JqueryPromiseProxy.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default class JqueryPromiseProxy { 4 | 5 | constructor() { 6 | this._thens = [] 7 | this._catches = [] 8 | } 9 | 10 | then(callback) { 11 | this._thens.push(callback) 12 | } 13 | 14 | catch(callback) { 15 | this._catches.push(callback) 16 | } 17 | 18 | applyToJqueryPromise(promise) { 19 | _.each(this._thens, function (callback) { 20 | promise.then(callback) 21 | }) 22 | _.each(this._catches, function (callback) { 23 | promise.catch(callback) 24 | }) 25 | } 26 | } -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/src/ajax-manager.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import _ from 'lodash' 3 | 4 | export default new Vue({ 5 | data (){ 6 | return { 7 | syncRoutes: [], 8 | 9 | // Routes to warn from config 10 | warnRoutes: {}, 11 | // Current warned routes (closures) 12 | urlsToWarn: [], 13 | 14 | hold: [], 15 | holdenRoutes: {}, 16 | 17 | activeUrls: {}, 18 | } 19 | }, 20 | methods: { 21 | isActive(url){ 22 | // Might be undefined so we convert to boolean 23 | return this.activeUrls[url] ? true : false 24 | }, 25 | warn(route, confirm){ 26 | let warn = { 27 | route, 28 | message: this.warnRoutes[route], 29 | confirm, 30 | } 31 | this.urlsToWarn.push(warn) 32 | }, 33 | requiresWarn(url){ 34 | return this.warnRoutes[url] ? true : false 35 | }, 36 | requiresHold(url){ 37 | return _.includes(this.hold, url) 38 | }, 39 | isSync(url){ 40 | return _.includes(this.syncRoutes, url) 41 | }, 42 | getEmptyPromise() { 43 | return { 44 | then: () => this, 45 | catch: () => this, 46 | } 47 | } 48 | } 49 | }) -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/src/components/api-monitor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/api_demo_2/src/components/warn-modal.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 70 | 71 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/env.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | 3 | global.ENV = { 4 | token: $('meta[name=token]').prop('content'), 5 | base: $('base').prop('href'), 6 | firebaseToken: $('meta[name=firebaseToken]').prop('content'), 7 | firebaseSource: $('meta[name=firebaseSource]').prop('content'), 8 | } 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/assets/js/plugins/globals.js: -------------------------------------------------------------------------------- 1 | // Expose global variables for debug purposes. 2 | 3 | import Vue from 'vue' 4 | window._Vue = Vue -------------------------------------------------------------------------------- /resources/assets/js/vuex/actions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export const loadRoutes = function ({dispatch}) { 4 | dispatch('SET_ROUTES_LOADING', true) 5 | dispatch('SET_ROUTES', []) 6 | 7 | this.$api_demo2.load({url: 'routes/index'}) 8 | .then((response) => { 9 | dispatch('SET_ROUTES_ERROR', false) 10 | dispatch('SET_ROUTES', response.data) 11 | dispatch('SET_ROUTES_LOADING', false) 12 | }) 13 | .catch((xhr, status, error) => { 14 | let response = { 15 | status: xhr.status + ' : ' + error, 16 | data: xhr.responseText 17 | } 18 | dispatch('SET_ROUTES_ERROR', response) 19 | dispatch('SET_ROUTES_LOADING', false) 20 | }) 21 | } 22 | 23 | export const loadRequests = function ({dispatch}) { 24 | // TODO split Firebase and JSON storage. 25 | if (ENV.firebaseToken && ENV.firebaseToken) { 26 | return 27 | } 28 | 29 | this.$api_demo2.load({url: 'requests/index'}) 30 | .then(function (response) { 31 | dispatch('SET_REQUESTS', response.data) 32 | }) 33 | } 34 | 35 | export const setResponse = ({dispatch}, response) => dispatch('SET_RESPONSE', response) 36 | 37 | export const setRequests = function ({dispatch}, requests) { 38 | dispatch('SET_REQUESTS', requests) 39 | } 40 | 41 | export const setRequestInfo = ({dispatch}, route) => dispatch('SET_REQUEST_INFO', route) 42 | 43 | export const setCurrentRequest = ({dispatch}, request) => { 44 | dispatch('SET_CURRENT_REQUEST', request) 45 | } 46 | 47 | export const scheduleRequest = ({dispatch}, request) => dispatch('SCHEDULE_REQUEST', _.cloneDeep(request)) 48 | -------------------------------------------------------------------------------- /resources/assets/js/vuex/store.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | 3 | const state = { 4 | infoMode: 'route', 5 | infoError: false, 6 | } 7 | 8 | import routes from '../components/routes/routes-module.js' 9 | import response from '../components/edit-block/response-viewer/response-viewer-module.js' 10 | import request from '../components/edit-block/request-editor-module.js' 11 | import search from '../components/search/search-module.js' 12 | import requests from '../components/requests/requests-module.js' 13 | import history from '../components/history/history-module.js' 14 | export default new Vuex.Store({ 15 | strict: true, 16 | state, 17 | modules: { 18 | routes, 19 | response, 20 | request, 21 | search, 22 | requests, 23 | history, 24 | } 25 | }) -------------------------------------------------------------------------------- /resources/assets/js/vuex/vuex-installer.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | Vue.use(Vuex) -------------------------------------------------------------------------------- /resources/assets/sass/_jsoneditor.sass: -------------------------------------------------------------------------------- 1 | div.jsoneditor 2 | border-color: transparent 3 | .ace_editor 4 | border: 1px solid lighten($grey, 10%) 5 | &.ace_focus 6 | border-color: $primary 7 | .ace_marker-layer 8 | .ace_active-line 9 | background-color: lighten($primary, 40%) 10 | .jsoneditor-menu 11 | background-color: $primary 12 | border-bottom-color: transparent 13 | .jsoneditor-frame 14 | padding: 0 15 | background-color: #0092a2 16 | input 17 | color: #c6faff 18 | line-height: 1.7 19 | margin: 0 20 | &:focus 21 | color: $black 22 | background-color: $white 23 | .jsoneditor-poweredBy 24 | color: $white 25 | button 26 | border-radius: 0 27 | background-color: $primary 28 | margin: 0 29 | &:hover,&:active,&:focus 30 | background-color: darken($primary, 10%) 31 | outline: 0 -------------------------------------------------------------------------------- /resources/assets/sass/_transitions.scss: -------------------------------------------------------------------------------- 1 | .list-sort { 2 | &-enter, &-leave { 3 | /* 4 | the state right after enter (enter from this state) 5 | and the state right before leave (leave to this state) 6 | */ 7 | opacity: 0; 8 | } 9 | &-enter-active, &-leave-active { 10 | transition: opacity .5s ease; /* applied during enter/leave transition */ 11 | } 12 | &-transition { 13 | transition: transform .5s cubic-bezier(.55,0,.1,1); /* applied during moving transition */ 14 | } 15 | &-move { 16 | border-color: red; 17 | transition: transform .5s cubic-bezier(.55,0,.1,1); /* applied during moving transition */ 18 | } 19 | } 20 | 21 | .fade-in { 22 | &-transition { 23 | transition: all 0.4s ease; 24 | } 25 | &-enter, &-leave { 26 | opacity: 0; 27 | } 28 | } 29 | 30 | .slip{ 31 | &-transition { 32 | transition: all .5s ease; 33 | overflow: hidden; 34 | max-height: 50px; 35 | } 36 | &-enter, &-leave { 37 | opacity: 0; 38 | max-height: 0; 39 | } 40 | } -------------------------------------------------------------------------------- /resources/assets/sass/api-tester.sass: -------------------------------------------------------------------------------- 1 | // Media fix. (hack) 2 | $widescreen: 1300px 3 | @import "../../../node_modules/bulma/bulma.sass" 4 | 5 | $fa-font-path: '../assets/fonts' 6 | @import "../../../node_modules/font-awesome/scss/font-awesome.scss" 7 | 8 | @import "transitions" 9 | 10 | // Bulma buttons have strange margins when all button content is one icon. 11 | .button.is-icon 12 | width: 35px 13 | .icon, 14 | .tag 15 | margin: 0 16 | 17 | body 18 | background-color: #f5f7fa 19 | font-family: "Roboto", "Helvetica", "Arial", sans-serif 20 | height: 100% 21 | 22 | html 23 | height: 100% 24 | 25 | .input.is-minimal 26 | border-top: 0 27 | border-left: 0 28 | border-right: 0 29 | border-radius: 0 30 | box-shadow: none 31 | 32 | .no-rounded-borders 33 | border-radius: 0 34 | 35 | // Jsoneditor colors 36 | @import "../../../node_modules/jsoneditor/dist/jsoneditor" 37 | @import 'jsoneditor' 38 | 39 | .no-padding 40 | padding: 0 !important 41 | 42 | .card-item 43 | margin: 4px 0 44 | border-right: 2px solid transparent 45 | border-bottom: 1px solid rgba(0, 0, 0, .025) 46 | -------------------------------------------------------------------------------- /resources/views/api-tester.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Laravel api tester 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Collections/RequestCollection.php: -------------------------------------------------------------------------------- 1 | offsetExists($id) ? $this->offsetGet($id) : null; 26 | } 27 | 28 | /** 29 | * Put new RequestEntity to collection. 30 | * 31 | * @param \Asvae\ApiTester\Entities\RequestEntity $request 32 | * 33 | * @return \Asvae\ApiTester\Entities\RequestEntity 34 | */ 35 | public function insert(RequestEntity $request) 36 | { 37 | $this->put($request->getId(), $request); 38 | 39 | return $request; 40 | } 41 | 42 | /** 43 | * Load data to collection. 44 | * 45 | * @param $data 46 | * @return static 47 | */ 48 | public function load($data) 49 | { 50 | foreach ($data as $row) { 51 | $this->put($row['id'], RequestEntity::createExisting($row)); 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Filters 60 | |-------------------------------------------------------------------------- 61 | | 62 | */ 63 | 64 | /** 65 | * Новые записи или измененные записи, которые не были помечены на удаление. 66 | * 67 | * @return static 68 | */ 69 | public function onlyDiff() 70 | { 71 | return $this->filter(function (RequestEntity $request) { 72 | return ($request->notExists() || $request->isDirty()) && $request->notMarkedToDelete(); 73 | }); 74 | } 75 | 76 | /** 77 | * Только не помеченные на удаление 78 | * 79 | * @return static 80 | */ 81 | public function onlyNotMarkedToDelete(){ 82 | return $this->filter(function (RequestEntity $request) { 83 | return $request->notMarkedToDelete(); 84 | }); 85 | } 86 | 87 | /** 88 | * Только помеченные на удаление. 89 | * 90 | * @return static 91 | */ 92 | public function onlyToDelete() 93 | { 94 | return $this->filter(function (RequestEntity $request) { 95 | return $request->markedToDelete(); 96 | }); 97 | } 98 | 99 | /** 100 | * Только существующие записи, не помеченные на удаление. 101 | * 102 | * @return static 103 | */ 104 | public function onlyExists() 105 | { 106 | return $this->filter(function (RequestEntity $request) { 107 | return $request->exists() && $request->notMarkedToDelete(); 108 | }); 109 | } 110 | 111 | /** 112 | * Новые записи, которых еще нет в базе. 113 | * 114 | * @return static 115 | */ 116 | public function onlyNotExists() 117 | { 118 | return $this->filter(function (RequestEntity $request) { 119 | return $request->notExists(); 120 | }); 121 | } 122 | 123 | /** 124 | * Существующие записи, которые были изменены. 125 | * 126 | * @return static 127 | */ 128 | public function onlyDirty() 129 | { 130 | return $this->filter(function (RequestEntity $request) { 131 | return $request->isDirty() && $request->exists(); 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Collections/RouteCollection.php: -------------------------------------------------------------------------------- 1 | $patterns 18 | * 19 | * @return static 20 | */ 21 | public function filterMatch($patterns) 22 | { 23 | $patterns = is_string($patterns) ? [$patterns] : $patterns; 24 | 25 | // String pattern is assumed to be path. 26 | foreach ($patterns as $key => $pattern) { 27 | if (is_string($pattern)) { 28 | $patterns[$key] = ['path' => $pattern]; 29 | } 30 | } 31 | 32 | return $this->filter(function ($route) use ($patterns) { 33 | // If any of patterns matches - route passes. 34 | foreach ($patterns as $pattern) { 35 | if ($this->isRouteMatchesPattern($route, $pattern)) { 36 | return true; 37 | } 38 | } 39 | 40 | // If all patterns don't match - route is filtered out. 41 | return false; 42 | }); 43 | } 44 | 45 | 46 | /** 47 | * Exclude routes that match patterns. 48 | * 49 | * @param array $patterns 50 | * 51 | * @return static 52 | */ 53 | public function filterExcept($patterns = []) 54 | { 55 | if (empty($patterns)) { 56 | return $this; 57 | } 58 | 59 | $toExclude = $this->filterMatch($patterns)->keys()->toArray(); 60 | 61 | return $this->except($toExclude); 62 | } 63 | 64 | /** 65 | * @param array $route 66 | * @param array $pattern 67 | * @return bool 68 | */ 69 | private function isRouteMatchesPattern(array $route, array $pattern) 70 | { 71 | foreach ($route as $key => $value) { 72 | if (! array_key_exists($key, $pattern)) { 73 | continue; 74 | } 75 | 76 | if(is_array($value)){ 77 | $value = implode(',', $value); 78 | } 79 | 80 | $regex = '#'.$pattern[$key].'#'; 81 | 82 | return ! ! preg_match($regex, $value); 83 | } 84 | 85 | return true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Contracts/RequestRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | fill($data); 40 | } 41 | 42 | /** 43 | * Fill attributes that can be filled. 44 | * 45 | * @param $data 46 | * 47 | * @return void 48 | */ 49 | public function fill(array $data) 50 | { 51 | $this->attributes = array_merge($this->attributes, $this->filterFillable($data)); 52 | } 53 | 54 | /** 55 | * Get the instance as an array. 56 | * 57 | * @return array return array representation of object. 58 | */ 59 | public function toArray() 60 | { 61 | return $this->attributes; 62 | } 63 | 64 | /** 65 | * Convert the object to its JSON representation. 66 | * 67 | * @param int $options 68 | * 69 | * @return string 70 | */ 71 | public function toJson($options = 0) 72 | { 73 | return json_encode($this->jsonSerialize(), $options); 74 | } 75 | 76 | /** 77 | * Return array with with key that can be filled. 78 | * 79 | * @param array $data 80 | * 81 | * @return array 82 | */ 83 | protected function filterFillable(array $data) 84 | { 85 | return array_only($data, $this->fillable); 86 | } 87 | 88 | /** 89 | * Whether a offset exists. 90 | * 91 | * @param mixed $offset An offset to check for. 92 | * 93 | * @return boolean true on success or false on failure. 94 | */ 95 | public function offsetExists($offset) 96 | { 97 | return array_key_exists($offset, $this->attributes); 98 | } 99 | 100 | /** 101 | * Offset to retrieve. 102 | * 103 | * @param mixed $offset The offset to retrieve. 104 | * 105 | * @return mixed Can return all value types. 106 | */ 107 | public function offsetGet($offset) 108 | { 109 | return $this->attributes[$offset]; 110 | } 111 | 112 | /** 113 | * Offset to set. 114 | * 115 | * @param mixed $offset The offset to assign the value to. 116 | * @param mixed $value The value to set. 117 | * 118 | * @return void 119 | */ 120 | public function offsetSet($offset, $value) 121 | { 122 | $this->attributes[$offset] = $value; 123 | } 124 | 125 | /** 126 | * Offset to unset. 127 | * 128 | * @param mixed $offset The offset to unset. 129 | * 130 | * @return void 131 | */ 132 | public function offsetUnset($offset) 133 | { 134 | unset($this->attributes[$offset]); 135 | } 136 | 137 | /** 138 | * Specify data which should be serialized to JSON 139 | * @link http://php.net/manual/en/jsonserializable.jsonserialize.php 140 | * @return mixed data which can be serialized by json_encode, 141 | * which is a value of any type other than a resource. 142 | * @since 5.4.0 143 | */ 144 | function jsonSerialize() 145 | { 146 | return $this->toArray(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Entities/RequestEntity.php: -------------------------------------------------------------------------------- 1 | attributes['id'] = $id; 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getId() 45 | { 46 | return $this->attributes['id']; 47 | } 48 | 49 | public function exists() 50 | { 51 | return $this->exists; 52 | } 53 | 54 | /** 55 | * @param $data 56 | * 57 | * @return static 58 | */ 59 | public static function createExisting($data) 60 | { 61 | $newRequest = new static($data); 62 | $newRequest->setId($data['id']); 63 | $newRequest->setExists(true); 64 | 65 | return $newRequest; 66 | } 67 | 68 | public function setExists($bool) 69 | { 70 | $this->exists = $bool; 71 | } 72 | 73 | public function markedToDelete() 74 | { 75 | return $this->toDelete; 76 | } 77 | 78 | public function setDirty($bool = true) 79 | { 80 | $this->dirty = $bool; 81 | } 82 | 83 | public function isDirty() 84 | { 85 | return $this->dirty; 86 | } 87 | 88 | public function update($data) 89 | { 90 | $this->setDirty(); 91 | $this->fill($data); 92 | } 93 | 94 | public function markToDelete($bool = true) 95 | { 96 | $this->toDelete = $bool; 97 | } 98 | 99 | public function notExists() 100 | { 101 | return !$this->exists(); 102 | } 103 | 104 | public function notMarkedToDelete() 105 | { 106 | return !$this->markedToDelete(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Entities/RouteInfo.php: -------------------------------------------------------------------------------- 1 | route = $route; 53 | $this->options = $options; 54 | $this->addMeta = config('api-tester.route_meta'); 55 | $this->analyzeRequests = config('api-tester.analyze_requests'); 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function toArray() 62 | { 63 | return array_merge([ 64 | 'name' => $this->route->getName(), 65 | 'methods' => $this->getMethods(), 66 | 'domain' => $this->route->domain(), 67 | 'path' => $this->preparePath(), 68 | 'action' => $this->route->getAction(), 69 | 'wheres' => $this->extractWheres(), 70 | 'errors' => $this->errors, 71 | ], $this->getMeta(), $this->options); 72 | } 73 | 74 | /** 75 | * Cross version get methods. 76 | * 77 | * @return array 78 | */ 79 | private function getMethods() 80 | { 81 | // Laravel <5.4 82 | if (method_exists($this->route, 'getMethods')) { 83 | return $this->route->getMethods(); 84 | } 85 | // Laravel 5.4+ 86 | return $this->route->methods(); 87 | } 88 | 89 | protected function extractWheres() 90 | { 91 | $prop = $this->getRouteReflection()->getProperty('wheres'); 92 | $prop->setAccessible(true); 93 | 94 | $wheres = $prop->getValue($this->route); 95 | 96 | // Хак, чтобы в json всегда был объект 97 | if (empty($wheres)) { 98 | return (object) []; 99 | } 100 | 101 | return $wheres; 102 | } 103 | 104 | /** 105 | * @return array 106 | */ 107 | function jsonSerialize() 108 | { 109 | return $this->toArray(); 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | protected function extractAnnotation() 116 | { 117 | $reflection = $this->getActionReflection(); 118 | 119 | if (! is_null($reflection)) { 120 | return $reflection->getDocComment(); 121 | } 122 | 123 | return ''; 124 | } 125 | 126 | protected function extractFormRequest() 127 | { 128 | $reflection = $this->getActionReflection(); 129 | 130 | if (is_null($reflection)) { 131 | return null; 132 | } 133 | 134 | foreach ($reflection->getParameters() as $parameter) { 135 | 136 | // TODO Write the reasoning behind following lines. 137 | try { 138 | $class = $parameter->getType() && !$parameter->getType()->isBuiltin() 139 | ? new \ReflectionClass($parameter->getType()->getName()) 140 | : null; 141 | } catch (\ReflectionException $e) { 142 | break; 143 | } 144 | 145 | // Если аргумент нетипизирован, значит он уже не будет затянут через DI, 146 | // И дальнейший обход не имеет смысла, так как все последующие аргументы 147 | // тоже не будут затянуты через DI, не зависимо от того типизированы они или нет. 148 | if (is_null($class)) { 149 | break; 150 | } 151 | 152 | // Если это форм-реквест. 153 | if (is_subclass_of($class->name, FormRequest::class && $this->analyzeRequests)) { 154 | 155 | // Для вызова нестатического метода на объекте, нам необходим инстанс объекта. 156 | // Мы используем build вместо make, чтобы избежать автоматического запуска валидации. 157 | $formRequest = app()->build($class->name); 158 | 159 | // Здесь используется метод call, чтобы разрешить зависимости. 160 | $rules = app()->call([$formRequest, 'rules']); 161 | 162 | return [ 163 | 'class' => $class->name, 164 | 'rules' => $rules, 165 | ]; 166 | } 167 | } 168 | 169 | return null; 170 | } 171 | 172 | protected function getRouteReflection() 173 | { 174 | if ($this->routeReflection) { 175 | return $this->routeReflection; 176 | } 177 | 178 | return $this->routeReflection = new \ReflectionClass($this->route); 179 | } 180 | 181 | /** 182 | * @return \ReflectionFunctionAbstract|null 183 | */ 184 | protected function getActionReflection() 185 | { 186 | if ($this->actionReflection) { 187 | return $this->actionReflection; 188 | } 189 | 190 | $uses = $this->route->getAction()['uses']; 191 | 192 | // Если это строка и она содержит @, значит мы имем дело с методом контроллера. 193 | if (is_string($uses) && Str::contains($uses, '@')) { 194 | list($controller, $action) = explode('@', $uses); 195 | 196 | // Если нет контроллера. 197 | if (! class_exists($controller)) { 198 | $this->setError('uses', 'controller does not exists'); 199 | 200 | return null; 201 | } 202 | 203 | // Если нет метода в контроллере. 204 | if (! method_exists($controller, $action)) { 205 | $this->setError('uses', 'controller@method does not exists'); 206 | 207 | return null; 208 | } 209 | 210 | return $this->actionReflection = new \ReflectionMethod($controller, 211 | $action); 212 | } 213 | 214 | if (is_callable($uses)) { 215 | return $this->actionReflection = new \ReflectionFunction($uses); 216 | } 217 | 218 | $this->setError('uses', 'route uses is not valid'); 219 | 220 | return null; 221 | } 222 | 223 | protected function preparePath() 224 | { 225 | $path = $this->getUri(); 226 | if ($path === '/') { 227 | return $path; 228 | } 229 | 230 | return trim($path, '/'); 231 | } 232 | 233 | /** 234 | * Backwards compatible uri getter. 235 | * 236 | * @return string 237 | */ 238 | protected function getUri(){ 239 | if (method_exists($this->route, 'getPath')){ 240 | // Laravel <5.4 241 | return $this->route->getPath(); 242 | } 243 | 244 | // Laravel 5.4+ 245 | return $this->route->uri(); 246 | } 247 | 248 | protected function setError($type, $text, $params = []) 249 | { 250 | $this->errors[$type] = trans($text, $params); 251 | } 252 | 253 | /** 254 | * @return array 255 | */ 256 | protected function getMeta() 257 | { 258 | if ($this->addMeta) { 259 | return [ 260 | 'annotation' => $this->extractAnnotation(), 261 | 'formRequest' => $this->extractFormRequest(), 262 | 'errors' => $this->errors, 263 | ]; 264 | } 265 | 266 | return []; 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Exceptions/ApiTesterException.php: -------------------------------------------------------------------------------- 1 | file($file, $root); 15 | } 16 | 17 | public function image($file) 18 | { 19 | $root = __DIR__.'/../../../resources/assets/build/img'; 20 | 21 | return $this->file($file, $root); 22 | } 23 | 24 | protected function file($file, $root) 25 | { 26 | $contents = file_get_contents($root.DIRECTORY_SEPARATOR.$file); 27 | $response = response($contents, 200, [ 28 | 'Content-Type' => $this->getFileContentType($file), 29 | ]); 30 | 31 | // Browser will cache files for 1 year. 32 | $secondsInYear = 60 * 60 * 24 * 365; 33 | $response->setSharedMaxAge($secondsInYear); 34 | $response->setMaxAge($secondsInYear); 35 | $response->setExpires(new DateTime('+1 year')); 36 | 37 | return $response; 38 | } 39 | 40 | public function font($file) 41 | { 42 | $root = __DIR__.'/../../../resources/assets/fonts'; 43 | 44 | return $this->file($file, $root); 45 | } 46 | 47 | /** 48 | * Figure out appropriate "Content-Type" header 49 | * by filename. 50 | * 51 | * @param $file 52 | * @return mixed 53 | */ 54 | protected function getFileContentType($file) 55 | { 56 | $array = explode('.', $file); 57 | $ext = end($array); 58 | 59 | $contentTypes = [ 60 | 'css' => 'text/css', 61 | 'js' => 'text/javascript', 62 | 'svg' => 'image/svg+xml', 63 | 'map' => 'text/css', 64 | 'woff' => 'application/x-font-woff', 65 | 'woff2' => 'application/x-font-woff2', 66 | ]; 67 | 68 | return $contentTypes[$ext]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 32 | } 33 | 34 | /** 35 | * @return mixed 36 | */ 37 | public function index() 38 | { 39 | $data = $this->repository->all(); 40 | 41 | return response(compact('data'), 200); 42 | } 43 | 44 | /** 45 | * 46 | * @param \Asvae\ApiTester\Http\Requests\StoreRequest $storeRequest 47 | * 48 | * @return \Illuminate\Http\Response 49 | */ 50 | public function store(StoreRequest $storeRequest) 51 | { 52 | $request = new RequestEntity($storeRequest->all()); 53 | 54 | $this->repository->persist($request); 55 | 56 | $this->repository->flush(); 57 | 58 | // TODO Serializable? 59 | return response(['data' => $request->toArray()], 201); 60 | } 61 | 62 | /** 63 | * @param Request $request 64 | * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response 65 | */ 66 | public function destroy(Request $request) 67 | { 68 | $id = $request->id; 69 | 70 | if (! $this->repository->exists($id)) { 71 | return response(null, 404); 72 | } 73 | 74 | $this->repository->remove($id); 75 | 76 | $this->repository->flush(); 77 | 78 | return response(null, 204); 79 | } 80 | 81 | /** 82 | * @param \Asvae\ApiTester\Http\Requests\UpdateRequest $request 83 | * @param string $request 84 | * 85 | * @return \Illuminate\Http\Response 86 | * @internal param int $id 87 | * 88 | */ 89 | public function update(UpdateRequest $request) 90 | { 91 | $requestEntity = $this->repository->find($request->id); 92 | 93 | // TODO What's happening in here? 94 | if (!$requestEntity instanceof RequestEntity) { 95 | return response(404); 96 | } 97 | 98 | $requestEntity->update($request->all()); 99 | $this->repository->flush(); 100 | 101 | return response(['data' => $requestEntity->toArray()], 200); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Http/Controllers/RouteController.php: -------------------------------------------------------------------------------- 1 | get( 24 | config('api-tester.include'), 25 | config('api-tester.exclude') 26 | ); 27 | 28 | return response()->json(compact('data')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Middleware/DetectRoute.php: -------------------------------------------------------------------------------- 1 | events = $dispatcher; 28 | } 29 | 30 | public function handle(Request $request, Closure $next) 31 | { 32 | // In case the request was sent by Api Tester and wanted route-info 33 | // we will halt the request and output route information instead. 34 | if ($request->header('X-Api-Tester') === static::ROUTE_INFO) { 35 | 36 | // Laravel 5.1 event 37 | $this->events->listen('router.matched', [$this, 'handleMatchedRoute']); 38 | 39 | // Laravel 5.2 event 40 | $this->events->listen(RouteMatched::class, 41 | function (RouteMatched $event) { 42 | $this->handleMatchedRoute($event->route); 43 | }); 44 | } 45 | 46 | return $next($request); 47 | } 48 | 49 | public function handleMatchedRoute($route){ 50 | response()->json([ 51 | 'data' => new RouteInfo($route), 52 | ])->send(); 53 | 54 | exit(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Middleware/PreventRedirect.php: -------------------------------------------------------------------------------- 1 | header('X-Api-Tester') === static::CATCH_REDIRECT_HEADER) { 23 | if ($response->isRedirection()) { 24 | /** 25 | * @var \Symfony\Component\HttpFoundation\RedirectResponse $response 26 | */ 27 | return response()->json(['data' => [ 28 | 'location' => $response->getTargetUrl(), 29 | 'status' => $response->getStatusCode(), 30 | ]])->header('X-Api-Tester', 'redirect'); 31 | } 32 | } 33 | 34 | return $response; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/Http/Requests/StoreRequest.php: -------------------------------------------------------------------------------- 1 | 'string|in:GET,HEAD,POST,PUT,PATCH,DELETE|required', 18 | 'path' => 'string|required', 19 | 'headers' => 'array', 20 | 'body' => '', 21 | ]; 22 | } 23 | 24 | public function authorize() 25 | { 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Requests/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | 'required' 18 | ]; 19 | } 20 | 21 | public function authorize(){ 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/routes.php: -------------------------------------------------------------------------------- 1 | get('/', [ 7 | 'as' => 'home', 8 | 'uses' => 'HomeController@index', 9 | ]); 10 | 11 | // Fetch all Laravel routes. 12 | $router->post('routes/index', 'RouteController@index'); 13 | 14 | $router->post('requests/index', 'RequestController@index'); 15 | $router->post('requests/store', 'RequestController@store'); 16 | $router->post('requests/update', 'RequestController@update'); 17 | $router->post('requests/destroy', 'RequestController@destroy'); 18 | 19 | // We won't publish library's assets. 20 | // Instead we'll pass them via app which is slower but fine for development. 21 | $router->group(['prefix' => 'assets'], function ($router) { 22 | 23 | $filePattern = '^([a-z0-9_\-\.]+)$'; 24 | 25 | $router->get('fonts/{_file}', [ 26 | 'as' => 'font', 27 | 'uses' => 'AssetsController@font' 28 | ])->where('_file', $filePattern); 29 | 30 | $router->get('img/{_file}', [ 31 | 'as' => 'image', 'uses' => 'AssetsController@image' 32 | ])->where('_file', $filePattern); 33 | 34 | $router->get('{_file}', [ 35 | 'as' => 'file', 36 | 'uses' => 'AssetsController@index' 37 | ])->where('_file', $filePattern); 38 | }); 39 | 40 | /** 41 | * This route is quite special as it prevents user from caching routes 42 | * while in development mode. Sorta fool-proof measure. 43 | * 44 | * How it works? Believe it or not, laravel won't allow you to cache 45 | * closure route. Hacky but works. 46 | * This route is debug only, hence in production 47 | * it isn't registered and route cache is allowed. 48 | */ 49 | $router->any('* routes should not be cached',[ 50 | 'as' => 'routes-should not be cached', 51 | 'uses' => function () { return 'Api-tester routes-should not be cached';}, 52 | ]); 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Providers/RepositoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(RouteRepositoryInterface::class, function (Container $app) { 32 | $repositories = []; 33 | foreach (config('api-tester.route_repositories') as $repository) { 34 | $repositories[] = $app->make($repository); 35 | } 36 | 37 | $routeCollection = $app->make(RouteCollection::class); 38 | 39 | return new RouteRepository($routeCollection, $repositories); 40 | }); 41 | 42 | $this->app->singleton( 43 | RequestRepositoryInterface::class, 44 | config('api-tester.request_repository') 45 | ); 46 | } 47 | 48 | 49 | /** 50 | * Get the services provided by the provider. 51 | * 52 | * @return array 53 | */ 54 | public function provides() 55 | { 56 | return [ 57 | RouteRepositoryInterface::class, 58 | RequestRepositoryInterface::class, 59 | ]; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | group([ 29 | 'as' => 'api-tester.', 30 | 'prefix' => config('api-tester.route'), 31 | 'namespace' => $this->getNamespace(), 32 | 'middleware' => $this->getMiddleware(), 33 | ], function () { 34 | $this->requireRoutes(); 35 | }); 36 | } 37 | 38 | /** 39 | * @param Router $router 40 | * @param Kernel|\Illuminate\Foundation\Http\Kernel $kernel 41 | */ 42 | public function boot(Router $router, Kernel $kernel) 43 | { 44 | $this->map($router); 45 | 46 | $this->kernel = $kernel; 47 | 48 | // The middleware is used to intercept every request with specific header 49 | // so that laravel can tell us, which route the request belongs to. 50 | $kernel->prependMiddleware(DetectRoute::class); 51 | $kernel->prependMiddleware(PreventRedirect::class); 52 | } 53 | 54 | protected function getMiddleware() 55 | { 56 | $middleware = config('api-tester.middleware'); 57 | 58 | return $middleware; 59 | } 60 | 61 | /** 62 | * Get module namespace 63 | * 64 | * @return string 65 | */ 66 | protected function getNamespace() 67 | { 68 | return 'Asvae\ApiTester\Http\Controllers'; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | protected function requireRoutes() 75 | { 76 | require __DIR__ . '/../Http/routes.php'; 77 | } 78 | 79 | /** 80 | * Register the service provider. 81 | * 82 | * @return void 83 | */ 84 | public function register() 85 | { 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Providers/StorageServiceProvide.php: -------------------------------------------------------------------------------- 1 | app->singleton(StorageInterface::class, function (Container $app) { 41 | // Defined driver 42 | $selectedDriver = config('api-tester.storage_driver'); 43 | $driverClassName = config('api-tester.storage_drivers')[$selectedDriver]; 44 | $requestCollection = $app->make(RequestCollection::class); 45 | 46 | if ($selectedDriver === 'firebase'){ 47 | $tokenGenerator = $app->make(TokenGenerator::class); 48 | $base = config('api-tester.storage_drivers.firebase.options.base'); 49 | 50 | return new FireBaseStorage($requestCollection, $tokenGenerator, $base); 51 | } 52 | 53 | if ($selectedDriver === 'file'){ 54 | $fileSystem = $app->make(Filesystem::class); 55 | $path = config('api-tester.storage_drivers.file.options.path'); 56 | 57 | return new JsonStorage($fileSystem, $requestCollection, $path); 58 | } 59 | 60 | throw new Exception("Driver $selectedDriver doesn't exist. Use either 'firebase' or 'file'."); 61 | }); 62 | 63 | // Регистрация токен-генератора. Привязывается к ключу а не классу, 64 | // чтобы не конфликтовать с пользовательским генератором токенов. 65 | $this->app->singleton('api-tester.token_generator', function (Container $app) { 66 | $config = $app['config']['api-tester.storage_drivers.firebase.token']; 67 | return (new TokenGenerator($config['secret'])) 68 | ->setOptions($config['options']) 69 | ->setData($config['data']); 70 | }); 71 | 72 | // Подсовываем генератор в сторэйдж 73 | $this->app 74 | ->when(FireBaseStorage::class) 75 | ->needs(TokenGenerator::class) 76 | ->give('api-tester.token_generator'); 77 | } 78 | 79 | /** 80 | * Get the services provided by the provider. 81 | * 82 | * @return array 83 | */ 84 | public function provides() 85 | { 86 | return [ 87 | 'api-tester.token_generator', 88 | StorageInterface::class, 89 | ]; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Providers/ViewServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(API_TESTER_PATH . '/resources/views', 'api-tester'); 21 | } 22 | 23 | public function boot(Factory $view) 24 | { 25 | $view->composer(['api-tester::api-tester'], ApiTesterComposer::class); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Repositories/RequestRepository.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 38 | $this->load(); 39 | } 40 | 41 | /** 42 | * Get data from storage and load it into collection. 43 | * @return void 44 | */ 45 | protected function load() 46 | { 47 | $this->requests = $this->storage->get(); 48 | } 49 | 50 | /** 51 | * Replace existing collection with data loaded from storage. 52 | */ 53 | protected function reload() 54 | { 55 | $this->requests = $this->requests->make($this->getDataFromStorage()); 56 | } 57 | 58 | /** 59 | * Get all stored data storage. 60 | * 61 | * @return mixed 62 | */ 63 | protected function getDataFromStorage() 64 | { 65 | return $this->storage->get(); 66 | } 67 | 68 | /** 69 | * @param int $id 70 | * 71 | * @return RequestEntity 72 | */ 73 | public function find($id) 74 | { 75 | return $this->requests->find($id); 76 | } 77 | 78 | /** 79 | * @param \Asvae\ApiTester\Entities\RequestEntity $request 80 | * 81 | * @return mixed 82 | */ 83 | public function persist(RequestEntity $request) 84 | { 85 | $request->setId(str_random()); 86 | $this->requests->insert($request); 87 | } 88 | 89 | /** 90 | * @param int $id 91 | * 92 | * @return bool 93 | */ 94 | public function exists($id) 95 | { 96 | return $this->requests->has($id); 97 | } 98 | 99 | /** 100 | * @return mixed 101 | */ 102 | public function all() 103 | { 104 | return $this->requests->values(); 105 | } 106 | 107 | /** 108 | * @param string $id 109 | */ 110 | public function remove($id) 111 | { 112 | $this->find($id)->markToDelete(); 113 | } 114 | 115 | /** 116 | * @return void 117 | */ 118 | public function flush() 119 | { 120 | $this->storage->put($this->requests); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Repositories/RouteDingoRepository.php: -------------------------------------------------------------------------------- 1 | routes = $collection; 27 | $standardsTree = $config['api.standardsTree']; 28 | $subtype = $config['api.subtype']; 29 | $defaultFormat = $config['api.defaultFormat']; 30 | 31 | foreach ($router->getAdapterRoutes() as $versionName => $versionGroup) { 32 | foreach ($versionGroup as $route) { 33 | $routeInfo = (new RouteInfo($route, [ 34 | 'router' => 'Dingo', 35 | 'version' => $versionName, 36 | 'headers' => [ 37 | [ 38 | 'key' => 'Accept', 39 | 'value' => "application/{$standardsTree}.{$subtype}.{$versionName}+{$defaultFormat}" 40 | ] 41 | ] 42 | ]))->toArray(); 43 | $this->routes->push($routeInfo); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * @param array $match 50 | * @param array $except 51 | * 52 | * @return \Asvae\ApiTester\Collections\RouteCollection 53 | */ 54 | public function get($match = [], $except = []) 55 | { 56 | return $this->routes->filterMatch($match) 57 | ->filterExcept($except) 58 | ->values(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Repositories/RouteLaravelRepository.php: -------------------------------------------------------------------------------- 1 | routes = $collection; 20 | 21 | foreach ($router->getRoutes() as $route) { 22 | $routeInfo = (new RouteInfo($route, ['router' => 'Laravel']))->toArray(); 23 | $this->routes->push($routeInfo); 24 | } 25 | } 26 | 27 | /** 28 | * @param array $match 29 | * @param array $except 30 | * 31 | * @return \Asvae\ApiTester\Collections\RouteCollection 32 | */ 33 | public function get($match = [], $except = []) 34 | { 35 | return $this->routes->filterMatch($match) 36 | ->filterExcept($except) 37 | ->values(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Repositories/RouteRepository.php: -------------------------------------------------------------------------------- 1 | routes = $routes; 28 | $this->repositories = $repositories; 29 | } 30 | 31 | /** 32 | * @param array $match 33 | * @param array $except 34 | * 35 | * @return mixed 36 | */ 37 | public function get($match = [], $except = []) 38 | { 39 | foreach ($this->repositories as $repository) { 40 | 41 | foreach ($repository->get($match, $except) as $route) { 42 | $this->routes->push($route); 43 | } 44 | } 45 | 46 | return $this->routes; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(API_TESTER_PATH . '/config/api-tester.php', 'api-tester'); 19 | 20 | // If Api Tester is disabled, we won't run any service providers. 21 | if (!$this->app['config']['api-tester.enabled']) { 22 | return; 23 | } 24 | $this->app->register(RouteServiceProvider::class); 25 | $this->app->register(RepositoryServiceProvider::class); 26 | $this->app->register(StorageServiceProvide::class); 27 | $this->app->register(ViewServiceProvider::class); 28 | } 29 | 30 | public function boot() 31 | { 32 | $this->publishes([API_TESTER_PATH . '/config/api-tester.php' => config_path('api-tester.php')], 'config'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Storages/FireBaseStorage.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 41 | $this->initFirebase($tokens, $base); 42 | } 43 | 44 | /** 45 | * Get data from resource. 46 | * 47 | * @return RequestCollection 48 | */ 49 | public function get() 50 | { 51 | $result = $this->getFromFireBase(); 52 | 53 | $data = $this->addHeadersIfEmpty($result); 54 | 55 | return $this->makeCollection($data); 56 | } 57 | 58 | /** 59 | * Put data to resource. 60 | * 61 | * @param $data RequestCollection 62 | * @return void 63 | */ 64 | public function put(RequestCollection $data) 65 | { 66 | $this->store($data->onlyDiff()); 67 | $this->delete($data->onlyToDelete()); 68 | } 69 | 70 | /** 71 | * @param $data 72 | */ 73 | private function store($data) 74 | { 75 | foreach ($data as $request) { 76 | $this->putToFireBase($request); 77 | } 78 | } 79 | 80 | 81 | /** 82 | * @param RequestCollection $data 83 | */ 84 | private function delete(RequestCollection $data) 85 | { 86 | foreach ($data as $request) { 87 | $this->deleteFromFirebase($request); 88 | } 89 | } 90 | 91 | /** 92 | * @param RequestEntity $request 93 | */ 94 | private function putToFireBase(RequestEntity $request) 95 | { 96 | $this->firebase->set('requests/' . $request->getId(), $request->jsonSerialize()); 97 | } 98 | 99 | /** 100 | * @param RequestEntity $request 101 | */ 102 | private function deleteFromFirebase(RequestEntity $request) 103 | { 104 | $this->firebase->delete('requests/' . $request->getId()); 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | private function getFromFireBase() 111 | { 112 | $result = json_decode($this->firebase->get('requests'), true); 113 | 114 | if ($result && is_string($result)) { 115 | throw new FireBaseException($result); 116 | } 117 | 118 | return $result ?: []; 119 | } 120 | 121 | /** 122 | * @param array $data 123 | * @return RequestCollection 124 | */ 125 | protected function makeCollection($data = []) 126 | { 127 | return $this->collection->make()->load($data); 128 | } 129 | 130 | private function initFirebase(TokenGenerator $tokens, $base) 131 | { 132 | $this->firebase = new FirebaseLib($base, $tokens->create()); 133 | } 134 | 135 | private function addHeadersIfEmpty($data) 136 | { 137 | foreach ($data as $key => $request) { 138 | if (!array_key_exists('headers', $request)) { 139 | $data[$key]['headers'] = []; 140 | } 141 | } 142 | 143 | return $data; 144 | } 145 | } -------------------------------------------------------------------------------- /src/Storages/JsonStorage.php: -------------------------------------------------------------------------------- 1 | files = $files; 54 | $this->collection = $collection; 55 | $path = explode('/', $path); 56 | $this->filename = array_pop($path); 57 | $this->path = implode('/', $path); 58 | } 59 | 60 | /** 61 | * Return path to folder that can contain file. 62 | * 63 | * @return string 64 | */ 65 | public function getPath() 66 | { 67 | return $this->path; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getFilename() 74 | { 75 | return $this->filename; 76 | } 77 | 78 | /** 79 | * Return full file path. 80 | * 81 | * @return string 82 | */ 83 | public function getFilePath() 84 | { 85 | return $this->getPath() . '/' . $this->getFilename(); 86 | } 87 | 88 | /** 89 | * Return array parsed from file content. 90 | * 91 | * @return RequestCollection 92 | */ 93 | public function get() 94 | { 95 | $fullPath = $this->getFilePath(); 96 | 97 | if ($this->files->exists($fullPath)) { 98 | 99 | $content = $this->files->get($fullPath); 100 | 101 | return $this->makeCollection($this->parseResult($content)); 102 | } 103 | 104 | return $this->makeCollection(); 105 | } 106 | 107 | /** 108 | * @param RequestCollection $data 109 | * @return void 110 | */ 111 | public function put(RequestCollection $data) 112 | { 113 | $this->createDirectoryIfNotExists(); 114 | 115 | $content = $this->prepareContent($data->onlyNotMarkedToDelete()); 116 | 117 | $this->files->put($this->getFilePath(), $content); 118 | } 119 | 120 | /** 121 | * Make directory path if not exists 122 | */ 123 | protected function createDirectoryIfNotExists() 124 | { 125 | if (!is_dir($this->getPath())) { 126 | $this->files->makeDirectory($this->getPath(), 0755, true); 127 | } 128 | } 129 | 130 | /** 131 | * Parse result form given string 132 | * 133 | * @param $content 134 | * 135 | * @return array 136 | */ 137 | protected function parseResult($content) 138 | { 139 | $data = []; 140 | 141 | foreach (explode(static::ROW_DELIMITER, $content) as $row) { 142 | if (empty($row)) { 143 | continue; 144 | } 145 | 146 | $data[] = json_decode($row, true); 147 | } 148 | 149 | return $data; 150 | } 151 | 152 | /** 153 | * Prepare content string from given data 154 | * 155 | * @param \Traversable|array $data 156 | * 157 | * @return string 158 | */ 159 | private function prepareContent($data) 160 | { 161 | $content = ''; 162 | 163 | foreach ($data as $row) { 164 | $content .= $this->convertToJson($row) . static::ROW_DELIMITER; 165 | } 166 | 167 | return $content; 168 | } 169 | 170 | /** 171 | * @param $data 172 | * 173 | * @return null|string 174 | */ 175 | private function convertToJson($data) 176 | { 177 | if (is_array($data)) { 178 | return json_encode($data); 179 | } 180 | 181 | if ($data instanceof Jsonable) { 182 | return $data->toJson(); 183 | } 184 | 185 | if ($data instanceof Arrayable) { 186 | return json_encode($data->toArray()); 187 | } 188 | 189 | return null; 190 | } 191 | 192 | /** 193 | * @param array $data 194 | * @return RequestCollection 195 | */ 196 | private function makeCollection($data = []) 197 | { 198 | return $this->collection->make()->load($data); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/View/Composers/ApiTesterComposer.php: -------------------------------------------------------------------------------- 1 | app = $app; 20 | $this->driver = $app['config']['api-tester']['storage_driver']; 21 | 22 | } 23 | 24 | public function compose(View $view) 25 | { 26 | $firebaseToken = null; 27 | $firebaseSource = null; 28 | 29 | if($this->driver === 'firebase'){ 30 | $firebaseToken = $this->app['api-tester.token_generator']->create(); 31 | $firebaseSource = $this->app['config']['api-tester.storage_drivers.firebase.options.base']; 32 | } 33 | 34 | $view->with(compact('firebaseToken', 'firebaseSource')); 35 | } 36 | } -------------------------------------------------------------------------------- /tests/JsonStorageTest.php: -------------------------------------------------------------------------------- 1 | dir = __DIR__ . '/tmp'; 50 | $this->storageFilePath = $this->dir . '/test.db'; 51 | 52 | $this->referenceData = [ 53 | ['id'=> '111', 'path' => 'aaaa'], 54 | ['id'=> '222', 'path' => 'bbbb'], 55 | ['id'=> '333', 'path' => 'cccc'], 56 | ['id'=> '444', 'path' => 'dddd'], 57 | ['id'=> '555', 'path' => 'eeee'], 58 | ]; 59 | 60 | $this->referenceCollection = (new RequestCollection())->load($this->referenceData); 61 | 62 | $this->referenceContent = "{\"path\":\"aaaa\",\"id\":\"111\"}\n{\"path\":\"bbbb\",\"id\":\"222\"}\n{\"path\":\"cccc\",\"id\":\"333\"}\n{\"path\":\"dddd\",\"id\":\"444\"}\n{\"path\":\"eeee\",\"id\":\"555\"}\n"; 63 | 64 | $fs = new Filesystem; 65 | 66 | if ($fs->isDirectory($this->dir)) { 67 | $fs->deleteDirectory($this->dir); 68 | } 69 | 70 | $this->storage = new JsonStorage( 71 | $fs, 72 | new RequestCollection(), 73 | $this->storageFilePath 74 | ); 75 | } 76 | 77 | public function testStoringData() 78 | { 79 | $this->referenceCollection->each(function(RequestEntity $value){ 80 | $value->setDirty(); $value->exists(); 81 | }); 82 | 83 | $this->storage->put($this->referenceCollection); 84 | 85 | $testRow = file_get_contents($this->storageFilePath); 86 | 87 | $this->assertEquals($this->referenceContent, $testRow); 88 | } 89 | 90 | public function testGettingData() 91 | { 92 | mkdir($this->dir, 0755, true); 93 | 94 | file_put_contents($this->storageFilePath, $this->referenceContent); 95 | 96 | $data = $this->storage->get(); 97 | 98 | $this->assertEquals($this->referenceCollection, $data); 99 | } 100 | 101 | public function testRemoveAll() 102 | { 103 | $this->storage->put(new RequestCollection()); 104 | $data = $this->storage->get(); 105 | 106 | $this->assertEquals(new RequestCollection(), $data); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/RequestCollectionTest.php: -------------------------------------------------------------------------------- 1 | requests = new RequestCollection(); 26 | $this->requestsCount = 10; 27 | $this->exampleRequest = [ 28 | 'path' => 'example/{param}', 29 | 'headers' => [], 30 | 'body' => [], 31 | ]; 32 | } 33 | 34 | /** 35 | * @covers Asvae\ApiTester\Collections\RequestCollection::load() 36 | */ 37 | public function testLoad() 38 | { 39 | $this->assertEquals(0, $this->requests->count()); 40 | 41 | $this->requests->load($this->generateData()); 42 | 43 | $this->assertEquals(10, $this->requests->count()); 44 | } 45 | 46 | /** 47 | * @covers Asvae\ApiTester\Collections\RequestCollection::find() 48 | */ 49 | public function testFind() 50 | { 51 | $this->hydrateRequest(); 52 | 53 | $request = $this->requests->first(); 54 | $foundRequest = $this->requests->find($request['id']); 55 | 56 | $this->assertTrue($request === $foundRequest); 57 | } 58 | 59 | /** 60 | * @covers Asvae\ApiTester\Collections\RequestCollection::insert() 61 | */ 62 | public function testInsert() 63 | { 64 | $this->hydrateRequest(); 65 | $request = m::mock(RequestEntity::class)->shouldReceive([ 66 | 'getId' => 5, 67 | 'offsetGet' => 5, 68 | 'offsetExists' => true, 69 | ])->andSet('id', 5)->mock(); 70 | 71 | $this->requests->insert($request); 72 | $foundRequest = $this->requests->where('id', $request['id'])->first(); 73 | 74 | $this->assertEquals($request, $foundRequest); 75 | } 76 | 77 | private function hydrateRequest() 78 | { 79 | $this->requests->load($this->generateData()); 80 | } 81 | 82 | private function generateData() 83 | { 84 | $data = []; 85 | 86 | for ($i = 1; $i <= $this->requestsCount; $i++) { 87 | 88 | $request = [ 89 | 'id' => $i, 90 | 'path' => str_replace('{param}', $i, 91 | $this->exampleRequest['path']), 92 | ]; 93 | 94 | $data[] = $request + $this->exampleRequest; 95 | } 96 | 97 | return $data; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/RouteCollectionTest.php: -------------------------------------------------------------------------------- 1 | 'GET', 21 | 'action' => '', 22 | 'name' => null, 23 | 'path' => 'api-tester', 24 | ]; 25 | 26 | protected function setUp() 27 | { 28 | $routes = $this->fakeRoutes([ 29 | [ 30 | 'method' => ['GET', 'HEAD'], 31 | 'path' => 'user', 32 | ], 33 | [ 34 | 'method' => ['GET', 'HEAD'], 35 | 'path' => 'user/{id}', 36 | ], 37 | [ 38 | 'method' => ['POST'], 39 | 'path' => 'user', 40 | ], 41 | [ 42 | 'method' => ['DELETE'], 43 | 'path' => 'user/{id}', 44 | ], 45 | [ 46 | 'method' => ['PUT', 'PATCH'], 47 | 'path' => 'user/{id}', 48 | ], 49 | [ 50 | 'method' => 'GET', 51 | 'path' => 'article', 52 | ], 53 | ]); 54 | 55 | $this->routes = new RouteCollection($routes); 56 | } 57 | 58 | private function fakeRoutes(array $routes) 59 | { 60 | $result = []; 61 | 62 | foreach ($routes as $route) { 63 | $result[] = array_replace($this->defaultRoute, $route); 64 | } 65 | 66 | return $result; 67 | } 68 | 69 | public function testMatchRoute() 70 | { 71 | $this->assertEquals(1, $this->routes->filterMatch(['article'])->count()); 72 | 73 | $this->assertEquals(5, $this->routes->filterMatch(['user'])->count()); 74 | } 75 | 76 | public function testMatchMethod() 77 | { 78 | $count = $this->routes->filterMatch([['method' => 'GET']])->count(); 79 | $this->assertEquals(3, $count); 80 | } 81 | 82 | public function testMatchMethods() 83 | { 84 | $count = $this->routes->filterMatch([ 85 | ['method' => 'POST'], 86 | ['method' => 'DELETE'], 87 | ])->count(); 88 | $this->assertEquals(2, $count); 89 | } 90 | 91 | public function testMatchRegex() 92 | { 93 | $this->assertEquals(2, $this->routes->filterMatch(['user$',])->count()); 94 | } 95 | 96 | public function testMatchEmptyPattern() 97 | { 98 | $this->assertEquals(6, $this->routes->filterMatch([[]])->count()); 99 | } 100 | 101 | public function testExcept() 102 | { 103 | $count = $this->routes->filterExcept([ 104 | ['method' => 'POST'], 105 | ['method' => 'DELETE'], 106 | ])->count(); 107 | $this->assertEquals(4, $count); 108 | } 109 | 110 | public function testEmptyExcept() 111 | { 112 | $count = $this->routes->filterExcept()->count(); 113 | $this->assertEquals(6, $count); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |