diff --git a/.env.example b/.env.example deleted file mode 100644 index b7becba..0000000 --- a/.env.example +++ /dev/null @@ -1,52 +0,0 @@ -APP_NAME=Laravel -APP_ENV=local -APP_KEY= -APP_DEBUG=true -APP_URL=http://localhost - -LOG_CHANNEL=stack -LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug - -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=laravel -DB_USERNAME=root -DB_PASSWORD= - -BROADCAST_DRIVER=log -CACHE_DRIVER=file -FILESYSTEM_DRIVER=local -QUEUE_CONNECTION=sync -SESSION_DRIVER=file -SESSION_LIFETIME=120 - -MEMCACHED_HOST=127.0.0.1 - -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 - -MAIL_MAILER=smtp -MAIL_HOST=mailhog -MAIL_PORT=1025 -MAIL_USERNAME=null -MAIL_PASSWORD=null -MAIL_ENCRYPTION=null -MAIL_FROM_ADDRESS=null -MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_APP_CLUSTER=mt1 - -MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" -MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/app/Http/Controllers/Api/V1/AuthController.php b/app/Http/Controllers/Api/V1/AuthController.php new file mode 100644 index 0000000..47ff909 --- /dev/null +++ b/app/Http/Controllers/Api/V1/AuthController.php @@ -0,0 +1,39 @@ +all(); + + $validator = Validator::make($data, [ + 'username' => 'required', + 'password' => 'required|min:6' + ]); + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + if (!Auth::attempt($data)) { + return $this->sendError('Unauthorised.',array('error'=>'Invalid login details')); + } + $user = User::where('username', $request['username'])->firstOrFail(); + $success['access_token'] = auth('api')->user()->createToken('auth_token')->plainTextToken; + $success['token_type'] = 'Bearer'; + return $success; + } + + public function logout (Request $request) { + + auth('api')->user()->currentAccessToken()->delete(); + return response(['message' => 'You have been successfully logged out.'], 200); + } +} diff --git a/app/Http/Controllers/Api/V1/BaseController.php b/app/Http/Controllers/Api/V1/BaseController.php new file mode 100644 index 0000000..8847c2f --- /dev/null +++ b/app/Http/Controllers/Api/V1/BaseController.php @@ -0,0 +1,44 @@ + true, + 'data' => $result, + 'message' => $message, + ]; + return response()->json($response, 200); + } + + + /** + * return error response. + * + * @return \Illuminate\Http\Response + */ + public function sendError($error, $errorMessages = [], $code = 200) + { + $response = [ + 'success' => false, + 'message' => $error, + ]; + + if(!empty($errorMessages)){ + $response['data'] = $errorMessages; + } + + return response()->json($response, $code); + } +} diff --git a/app/Http/Controllers/Api/V1/ProjectController.php b/app/Http/Controllers/Api/V1/ProjectController.php new file mode 100644 index 0000000..9e117d7 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ProjectController.php @@ -0,0 +1,84 @@ +user()->id); + if(isset($cachedProjects)) { + $projects = json_decode($cachedProjects, FALSE); + return new ProjectResource($projects); + }else { + // and then you can get query log + $query = Project::query(); + $q = $request->get('q',null); + $pageIndex = $request->get('pageIndex',0); + $pageSize = $request->get('pageSize',3); + $sortBy = $request->get('sortBy','name'); + $sortDirection = $request->get('sortDirection','ASC'); + + if (!empty($q))$query->where('name','like',$request->get('q').'%'); + $query->orderBy($sortBy, $sortDirection); + $query->offset(($pageIndex)*$pageSize); + $query->limit($pageSize); + $projects = $query->get(); + + Redis::set('projects_' . auth('api')->user()->id, $projects,'EX',10); + return new ProjectResource($projects); + } + } + + public function show($id) + { + $project = Project::find($id); + return new ProjectResource($project); + } + + public function store(Request $request) + { + $data = $request->all(); + $validator = Validator::make($data, [ + 'name' => 'required|unique:projects,name', + ]); + $data['name'] = $data['name']; + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + $Project = Project::create($data); + return new ProjectResource($Project); + } + + public function update(Request $request, $id) + { + $project = Project::findOrFail($id); + $data = $request->all(); + + $validator = Validator::make($data, [ + 'name' => 'required|unique:projects,name,'.$id, + ]); + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + $project->update($data); + return new ProjectResource($project); + } + + public function destroy(Request $request, $id) + { + $project = Project::findOrFail($id); + $project->delete(); + return response()->json([ + 'message' => 'Project deleted successfully' + ], 200); + } +} diff --git a/app/Http/Controllers/Api/V1/TaskController.php b/app/Http/Controllers/Api/V1/TaskController.php new file mode 100644 index 0000000..0d4ec83 --- /dev/null +++ b/app/Http/Controllers/Api/V1/TaskController.php @@ -0,0 +1,68 @@ +all(); + $validator = Validator::make($data, [ + 'title' => 'required', + 'status' => 'required', + 'project_id' => 'required', + 'user_id' => 'required', + ]); + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + $Task = Task::create($data); + return new TaskResource($Task); + } + + public function update(Request $request, $id) + { + $task = Task::findOrFail($id); + $data = $request->all(); + + $validator = Validator::make($data, [ + 'title' => 'required', + 'status' => 'required', + ]); + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + $task->update($data); + return new TaskResource($task); + } + + public function destroy(Request $request, $id) + { + $task = Task::findOrFail($id); + $task->delete(); + return response()->json([ + 'message' => 'Task deleted successfully' + ], 200); + } +} diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php new file mode 100644 index 0000000..5c0168b --- /dev/null +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -0,0 +1,73 @@ +middleware('CheckBlackList'); + $data = $request->all(); + $validator = Validator::make($data, [ + 'username' => 'required|unique:users,username', + 'password' => 'required|min:6', + ]); + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + + $data['username'] = $data['username']; + + $data['password'] = Hash::make($data['password']); + $User = User::create($data); + return new UserResource($User); + } + + public function update(Request $request, $id) + { + $user = User::findOrFail($id); + $data = $request->all(); + + $validator = Validator::make($data, [ + 'username' => 'required|unique:users,username,'.$id, + 'password' => 'required|min:6', + ]); + + + + if($validator->fails()){ + return $this->sendError('Validation Error.', $validator->errors()); + } + $data['password'] = Hash::make($data['password']); + + $user->update($data); + return new UserResource($user); + } + + public function destroy(Request $request, $id) + { + $user = User::findOrFail($id); + $user->delete(); + return response()->json([ + 'message' => 'User deleted successfully' + ], 200); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index d3722c2..22d820d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -40,7 +40,7 @@ class Kernel extends HttpKernel ], 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, + \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], @@ -63,5 +63,6 @@ class Kernel extends HttpKernel 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'accountRole' => \App\Http\Middleware\AccountRole::class, ]; } diff --git a/app/Http/Middleware/AccountRole.php b/app/Http/Middleware/AccountRole.php new file mode 100644 index 0000000..685e191 --- /dev/null +++ b/app/Http/Middleware/AccountRole.php @@ -0,0 +1,28 @@ +user()->role == $type) { + return $next($request); + } + $response = [ + 'success' => false, + 'message' => 'You don\'t have permission to access this api.', + ]; + return response()->json($response); + } +} diff --git a/app/Http/Resources/ProjectResource.php b/app/Http/Resources/ProjectResource.php new file mode 100644 index 0000000..ba96ecf --- /dev/null +++ b/app/Http/Resources/ProjectResource.php @@ -0,0 +1,13 @@ +hasOne(User::class); + } + + public function projects() + { + return $this->hasOne(Project::class); + } + +} diff --git a/app/Models/User.php b/app/Models/User.php index 8996368..017ca8a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,43 +2,18 @@ namespace App\Models; -use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; -use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; +use Illuminate\Notifications\Notifiable; +use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable; - - /** - * The attributes that are mass assignable. - * - * @var array - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * The attributes that should be cast. - * - * @var array - */ - protected $casts = [ - 'email_verified_at' => 'datetime', - ]; + use HasFactory, HasApiTokens, Notifiable; + protected $fillable = ['username', 'password', 'role']; + + public function Task() + { + return $this->belongsTo(Task::class); + } } diff --git a/config/auth.php b/config/auth.php index d8c6cee..377e98a 100644 --- a/config/auth.php +++ b/config/auth.php @@ -40,6 +40,11 @@ 'driver' => 'session', 'provider' => 'users', ], + 'api' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + 'hash' => false, + ], ], /* diff --git a/config/database.php b/config/database.php index b42d9b3..66d8670 100644 --- a/config/database.php +++ b/config/database.php @@ -119,7 +119,7 @@ 'redis' => [ - 'client' => env('REDIS_CLIENT', 'phpredis'), + 'client' => env('REDIS_CLIENT', 'predis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php new file mode 100644 index 0000000..b9e09cd --- /dev/null +++ b/database/factories/ProjectFactory.php @@ -0,0 +1,21 @@ + $this->faker->unique()->name(), + ]; + } +} diff --git a/database/factories/TaskFactory.php b/database/factories/TaskFactory.php new file mode 100644 index 0000000..95780b0 --- /dev/null +++ b/database/factories/TaskFactory.php @@ -0,0 +1,26 @@ + $this->faker->text(25), + 'description' => $this->faker->sentence(2), + 'status'=>$this->faker->randomElement(['not_started']), + 'user_id'=> $this->faker->randomElement(User::all())['id'], + 'project_id'=> $this->faker->randomElement(Project::all())['id'], + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a3eb239..9868738 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -3,7 +3,6 @@ namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Str; class UserFactory extends Factory { @@ -15,25 +14,9 @@ class UserFactory extends Factory public function definition() { return [ - 'name' => $this->faker->name(), - 'email' => $this->faker->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), + 'username' => $this->faker->unique()->userName(), + 'password' => '$2y$10$QY1v7OuVtqwOc7SE88wqbej7K35KYfyltM9Ag90LnVpL3lbcFaIrC', + 'role'=>$this->faker->randomElement(['admin','product_owner','team_member']), ]; } - - /** - * Indicate that the model's email address should be unverified. - * - * @return \Illuminate\Database\Eloquent\Factories\Factory - */ - public function unverified() - { - return $this->state(function (array $attributes) { - return [ - 'email_verified_at' => null, - ]; - }); - } } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2022_09_02_115349_create_users_table.php similarity index 76% rename from database/migrations/2014_10_12_000000_create_users_table.php rename to database/migrations/2022_09_02_115349_create_users_table.php index 621a24e..fb3f363 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2022_09_02_115349_create_users_table.php @@ -15,11 +15,9 @@ public function up() { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); + $table->string('username')->unique(); $table->string('password'); - $table->rememberToken(); + $table->enum('role',array('admin','product_owner','team_member')); $table->timestamps(); }); } diff --git a/database/migrations/2022_09_02_120355_create_projects_table.php b/database/migrations/2022_09_02_120355_create_projects_table.php new file mode 100644 index 0000000..950d255 --- /dev/null +++ b/database/migrations/2022_09_02_120355_create_projects_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('projects'); + } +} diff --git a/database/migrations/2022_09_02_121145_create_tasks_table.php b/database/migrations/2022_09_02_121145_create_tasks_table.php new file mode 100644 index 0000000..a7fd0f9 --- /dev/null +++ b/database/migrations/2022_09_02_121145_create_tasks_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->enum('status',array('not_started','in_progress','ready_for_test','completed')); + $table->unsignedBigInteger('user_id'); + $table->unsignedBigInteger('project_id'); + $table->timestamps(); + + $table->foreign('user_id') + ->references('id') + ->on('users') + ->onDelete('cascade'); + $table->foreign('project_id') + ->references('id') + ->on('projects') + ->onDelete('cascade'); + + }); + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('tasks'); + } +} diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..a42a46f --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,41 @@ +insert([ + 'username' => 'admin', + 'password' => Hash::make('admin@123'), + 'role' => 'admin', + ]); + DB::table('users')->insert([ + 'username' => 'product.owner', + 'password' => Hash::make('productowner@123'), + 'role' => 'product_owner', + ]); + DB::table('users')->insert([ + 'username' => 'team.member.1', + 'password' => Hash::make('teammember1@123'), + 'role' => 'team_member', + + ]); + DB::table('users')->insert([ + 'username' => 'team.member.2', + 'password' => Hash::make('teammember2@123'), + 'role' => 'team_member', + ]); + } +} \ No newline at end of file diff --git a/docs/Laravel-test.postman_collection.json b/docs/Laravel-test.postman_collection.json new file mode 100644 index 0000000..e43aa6b --- /dev/null +++ b/docs/Laravel-test.postman_collection.json @@ -0,0 +1,512 @@ +{ + "id": "a7fef902-685a-f986-f259-a6e6897e4dda", + "name": "Laravel-test", + "description": "", + "order": [ + "086abfb1-e153-e101-45c8-1674db670ac3", + "507a56f8-3597-3c15-41a6-9da2616dfe1b", + "5bf9914d-172e-9b91-5f0a-66962ee516b8", + "bf54aa0a-9bd8-7a5d-8eb7-23e355741669", + "58844809-ac9c-7d40-7721-bca8cd83a9fa", + "1c4de4c9-accb-c2b9-5ac7-b3a4a6ad1425", + "a01792a8-c7dc-84da-471f-997652a03f33", + "425762f6-ed9c-4c08-e452-7e7fd59e0ce8", + "9ccdd52c-b60b-2b5c-2f49-465265879cc5", + "0e6bf5ab-a4fe-9251-7974-fd71cd929868", + "d78d5786-6bb9-28c2-3443-43edb0b9317f" + ], + "folders": [], + "folders_order": [], + "timestamp": 0, + "owner": 0, + "public": false, + "requests": [ + { + "id": "086abfb1-e153-e101-45c8-1674db670ac3", + "headers": "", + "headerData": [], + "url": "http://127.0.0.1:8000/api/v1/login", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [ + { + "key": "username", + "value": "admin", + "description": "", + "type": "text", + "enabled": true + }, + { + "key": "password", + "value": "admin@123", + "description": "", + "type": "text", + "enabled": true + } + ], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662479177359, + "name": "Login", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "0e6bf5ab-a4fe-9251-7974-fd71cd929868", + "headers": "Accept: application/json\nAuthorization: Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/tasks", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [ + { + "key": "title", + "value": "Testing task for laravel project", + "type": "text" + }, + { + "key": "description", + "value": "Create listed module in api User, Project, Task", + "type": "text" + }, + { + "key": "project_id", + "value": "1", + "type": "text" + }, + { + "key": "user_id", + "value": "3", + "type": "text" + }, + { + "key": "status", + "value": "not_started", + "description": "", + "type": "text", + "enabled": true + } + ], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662480271695, + "name": "Create task", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "1c4de4c9-accb-c2b9-5ac7-b3a4a6ad1425", + "name": "Patch resource", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "method": "PATCH", + "helperAttributes": {}, + "headers": "Accept: application/json", + "dataMode": "urlencoded", + "data": [ + { + "key": "username", + "value": "team_member", + "type": "text" + }, + { + "key": "password", + "value": "123456", + "type": "text" + }, + { + "key": "role_id", + "value": "3", + "type": "text" + } + ], + "rawModeData": "", + "url": "http://laravel-coding-test-level-2.test/api/v1/users/631ddd759d04487a868afdd1b7ad0cae", + "responses": [], + "pathVariableData": [], + "queryParams": [], + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + } + ], + "collection_id": "a7fef902-685a-f986-f259-a6e6897e4dda" + }, + { + "id": "425762f6-ed9c-4c08-e452-7e7fd59e0ce8", + "headers": "Accept: application/json\nAuthorization: Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/projects?q=Test Project&pageIndex=0&pageSize=3&sortBy=name&sortDirection=DESC", + "queryParams": [ + { + "key": "q", + "value": "Test Project", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "pageIndex", + "value": "0", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "pageSize", + "value": "3", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "sortBy", + "value": "name", + "equals": true, + "description": "", + "enabled": true + }, + { + "key": "sortDirection", + "value": "DESC", + "equals": true, + "description": "", + "enabled": true + } + ], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662480118724, + "name": "Projects with redis pagination", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "507a56f8-3597-3c15-41a6-9da2616dfe1b", + "headers": "Accept: application/json\nAuthorization: Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/users", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662479259248, + "name": "Get all resources", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "58844809-ac9c-7d40-7721-bca8cd83a9fa", + "headers": "Accept: application/json\nAuthorization: Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/users/2", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "PUT", + "data": [ + { + "key": "username", + "value": "product.owner.12", + "description": "", + "type": "text", + "enabled": true + }, + { + "key": "password", + "value": "987654", + "type": "text" + } + ], + "dataMode": "urlencoded", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662479552466, + "name": "Update resource", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "5bf9914d-172e-9b91-5f0a-66962ee516b8", + "headers": "Accept: application/json\nAuthorization: Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/users/2", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "GET", + "data": [], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662479336726, + "name": "Get one resource", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "9ccdd52c-b60b-2b5c-2f49-465265879cc5", + "headers": "Accept: application/json\nAuthorization: Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 3|SKtoOaguLVBrURs8J3auBvLeDI9GDYD76lSL06c1", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/projects", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [ + { + "key": "name", + "value": "Test Project", + "type": "text" + } + ], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662480120603, + "name": "Create project", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "a01792a8-c7dc-84da-471f-997652a03f33", + "headers": "Accept: application/json\nAuthorization: Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/users/6", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "DELETE", + "data": [], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662480122471, + "name": "Delete resource", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "bf54aa0a-9bd8-7a5d-8eb7-23e355741669", + "headers": "Accept: application/json\nAuthorization: Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 2|IMtZ9nA4z0aOAFnUkCzkYaOCT9nilzjeYlAy26mj", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/users", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "POST", + "data": [ + { + "key": "username", + "value": "product.owner.1", + "type": "text" + }, + { + "key": "password", + "value": "123456", + "type": "text" + }, + { + "key": "role", + "value": "product_owner", + "type": "text" + } + ], + "dataMode": "params", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662479416946, + "name": "Create resource", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + }, + { + "id": "d78d5786-6bb9-28c2-3443-43edb0b9317f", + "headers": "Accept: application/json\nAuthorization: Bearer 4|mZJqNPcSaWdUaDQkNBgi5Pnr1MsCBg79XGcyYZB3\n", + "headerData": [ + { + "key": "Accept", + "value": "application/json", + "description": "", + "enabled": true + }, + { + "key": "Authorization", + "value": "Bearer 4|mZJqNPcSaWdUaDQkNBgi5Pnr1MsCBg79XGcyYZB3", + "description": "", + "enabled": true + } + ], + "url": "http://127.0.0.1:8000/api/v1/tasks/1", + "queryParams": [], + "preRequestScript": null, + "pathVariables": {}, + "pathVariableData": [], + "method": "PUT", + "data": [ + { + "key": "status", + "value": "in_progress", + "type": "text" + } + ], + "dataMode": "urlencoded", + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1662480713193, + "name": "Update Task Status", + "description": "", + "collectionId": "a7fef902-685a-f986-f259-a6e6897e4dda", + "responses": [] + } + ] +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index eb6fa48..5332eba 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,7 +2,10 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; - +use App\Http\Controllers\Api\V1\AuthController; +use App\Http\Controllers\Api\V1\UserController; +use App\Http\Controllers\Api\V1\ProjectController; +use App\Http\Controllers\Api\V1\TaskController; /* |-------------------------------------------------------------------------- | API Routes @@ -14,6 +17,22 @@ | */ -Route::middleware('auth:sanctum')->get('/user', function (Request $request) { - return $request->user(); +Route::name('login')->post('/v1/login', [AuthController::class,'login']); + + +Route::group(['middleware' => ['auth:api']], function () { + Route::name('logout')->post('/v1/logout', [AuthController::class,'logout']); +}); + +Route::group(['middleware' => ['auth:api','accountRole:admin']], function () { + Route::resource('/v1/users', UserController::class); +}); + +Route::group(['middleware' => ['auth:api','accountRole:product_owner']], function () { + Route::resource('v1/projects', ProjectController::class); + Route::resource('v1/tasks', TaskController::class); +}); + +Route::group(['middleware' => ['auth:api','accountRole:team_member']], function () { + Route::put('v1/tasks', [TaskController::class,'update']); }); diff --git a/tests/Feature/ProjectTest.php b/tests/Feature/ProjectTest.php new file mode 100644 index 0000000..7835702 --- /dev/null +++ b/tests/Feature/ProjectTest.php @@ -0,0 +1,68 @@ +get('/'); + + $response->assertStatus(200); + } + + public function test_create_project() + { + $this->testInit(); + //login as product owner and create project + $this->authLoginRole('product_owner'); + $project = [ + "name" => "Test Project 1", + ]; + $createProjectResponse = $this->post('/api/v1/projects', $project); + $createProjectResponse->assertStatus(201); + $createProjectResponse->assertJsonPath("data.name", "Test Project 1"); + $projectObj = json_decode($createProjectResponse->getContent()); + $teamMember1Id = 3;$teamMember2Id = 4; + $taskData1 = [ + "title" => "Test Task 1", + "description" => "description 1=> test Task For User", + "project_id" => $projectObj->data->id, + "user_id" => $teamMember1Id, + "status" => 'not_started' + ]; + $createTaskResponse1 = $this->post('/api/v1/tasks', $taskData1); + $createTaskResponse1->assertStatus(201); + $createTaskResponse1->assertJsonPath("data.title", "Test Task 1"); + $createTaskResponse1->assertJsonPath("data.status", 'not_started'); + $createTaskResponse1->assertJsonPath("data.project_id", $projectObj->data->id); + $createTaskResponse1->assertJsonPath("data.user_id", $teamMember1Id); + + $taskData2 = [ + "title" => "Test Task 2", + "description" => "description 2=> test Task For User", + "project_id" => $projectObj->data->id, + "user_id" => $teamMember2Id, + "status" => 'not_started' + ]; + $createTaskResponse2 = $this->post('/api/v1/tasks', $taskData2); + $createTaskResponse2->assertStatus(201); + $createTaskResponse2->assertJsonPath("data.title", "Test Task 2"); + $createTaskResponse2->assertJsonPath("data.status", 'not_started'); + $createTaskResponse2->assertJsonPath("data.project_id", $projectObj->data->id); + $createTaskResponse2->assertJsonPath("data.user_id", $teamMember2Id); + + + $this->testClear(); + } + +} diff --git a/tests/Feature/TeamMemberTest.php b/tests/Feature/TeamMemberTest.php new file mode 100644 index 0000000..da7ae3f --- /dev/null +++ b/tests/Feature/TeamMemberTest.php @@ -0,0 +1,37 @@ +get('/'); + + $response->assertStatus(200); + } + + public function test_team_member_change_task_status() + { + $this->testInit(); + + $this->authLoginRole('team_member'); + + $updateResponse = $this->patch('api/v1/tasks/1', [ + "status" => 'in_progress' + ]); + $updateResponse->assertStatus(200); + + $this->testClear(); + } +} diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php new file mode 100644 index 0000000..2a06026 --- /dev/null +++ b/tests/Feature/UserTest.php @@ -0,0 +1,43 @@ +get('/'); + + $response->assertStatus(200); + } + + public function test_user_create() + { + $this->testInit(); + $this->authLoginRole('admin'); + $userData = [ + "username" => "arjun.solanki", + "password"=> "arjun@123", + "role"=>"team_member", + ]; + $createUserResponse = $this->post('/api/v1/users', $userData); + $checkUserData = [ + "username" => "arjun.solanki", + "role"=>"team_member", + ]; + foreach ($checkUserData as $userKey => $userValue) { + $createUserResponse->assertJsonPath("data.{$userKey}", $userValue); + } + $this->testClear(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a..12439ae 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,4 +7,38 @@ abstract class TestCase extends BaseTestCase { use CreatesApplication; + + protected function testInit() { + $this->artisan('db:wipe'); + $this->artisan('migrate'); + $this->artisan('db:seed --class="UserSeeder"'); + } + + protected function authLoginRole($role = '') { + $loginApiLink = '/api/v1/login'; + $return = array(); + if ($role === 'admin') { + $return = $this->post($loginApiLink, [ + "username" => "admin", + "password" => "admin@123", + ]); + } + if ($role === 'product_owner') { + $return = $this->post($loginApiLink, [ + "username" => "product.owner", + "password" => "productowner@123", + ]); + } + if ($role === 'team_member') { + $return = $this->post($loginApiLink, [ + "username" => "team.member.1", + "password" => "teammember1@123", + ]); + } + return $return; + } + + protected function testClear() { + $this->artisan('db:wipe'); + } }