diff --git a/app/Clients/Consumer/ConsumerClient.php b/app/Clients/Consumer/ConsumerClient.php index 2bd1a5d..326b102 100644 --- a/app/Clients/Consumer/ConsumerClient.php +++ b/app/Clients/Consumer/ConsumerClient.php @@ -25,18 +25,18 @@ public function __construct() ]); } - public function updateUser(User $user): void + public function updateUser(User $user, Settings $settings): void { $uri = $this->baseVersionedUrl.'/settings'; /** @var ConnectedAccount $account */ - $account = $user->accounts()->where('provider', 'twitch')->first(); - /** @var Settings $settings */ - $settings = $user->settings()->with(['occupation', 'effect', 'color'])->first(); + $account = $user->accounts->first(fn ($account) => $account->provider == 'twitch'); $payload = [ 'user_id' => (int) $account->provider_user_id, 'locale' => $settings->locale, + 'enabled' => $settings->enabled, + 'channel_id' => $settings->channel_id, 'occupation' => [ 'name' => $settings->occupation->name, 'translation_key' => $settings->occupation->translation_key, diff --git a/app/DTO/AuthenticationDTO.php b/app/DTO/AuthenticationDTO.php index 350b4ae..01f17de 100644 --- a/app/DTO/AuthenticationDTO.php +++ b/app/DTO/AuthenticationDTO.php @@ -13,6 +13,10 @@ public function __construct( public static function factory(AuthorizationDTO $authorization, User $user): AuthenticationDTO { + // TODO: remove after releasing the next version. this is a workaround to not break the actual implementation + // of settings feature + $user->settings = $user->settings->first(fn ($settings) => $settings->channel_id = 'global'); + return new AuthenticationDTO( authorization: $authorization, user: $user, diff --git a/app/Http/Controllers/Api/V1/AuthenticatedUserController.php b/app/Http/Controllers/Api/V1/AuthenticatedUserController.php index ac1f292..60f8dcf 100644 --- a/app/Http/Controllers/Api/V1/AuthenticatedUserController.php +++ b/app/Http/Controllers/Api/V1/AuthenticatedUserController.php @@ -6,23 +6,54 @@ use App\Http\Controllers\Controller; use App\Http\Requests\SettingsRequest; use App\Models\Settings\Settings; +use App\Models\User; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class AuthenticatedUserController extends Controller { public function __construct(private ConsumerClient $client) {} + public function getSettings(Request $request): JsonResponse + { + /** @var User $user */ + $user = $request->user(); + + $settingsQuery = $user->settings(); + $fetcheableSettings = ['global']; + + if ($channelId = $request->get('channel_id')) { + $fetcheableSettings[] = $channelId; + } + + $response = $settingsQuery->whereIn('channel_id', $fetcheableSettings) + ->paginate(); + + return response()->json($response); + } + public function putSettings(SettingsRequest $request): JsonResponse { $validatedSettings = $request->validated(); - $userSettings = $request->user()->settings(); - $userSettings->update($validatedSettings); - /** @var Settings $response */ - $this->client->updateUser($request->user()->refresh()); + $request + ->user() + ->settings() + ->updateOrCreate([ + 'channel_id' => $validatedSettings['channel_id'], + ], $validatedSettings); - $response = $userSettings->with(['occupation', 'color', 'effect'])->first(); + /** @var User $user */ + $user = $request + ->user() + ->refresh() + ->with(['accounts', 'settings.occupation', 'settings.effect', 'settings.color']) + ->first(); - return response()->json($response); + $settings = $user->settings->first(fn (Settings $settings) => $settings->channel_id == $validatedSettings['channel_id']); + + $this->client->updateUser($user, $settings); + + return response()->json($settings); } } diff --git a/app/Http/Requests/SettingsRequest.php b/app/Http/Requests/SettingsRequest.php index afab9fd..5553a2d 100644 --- a/app/Http/Requests/SettingsRequest.php +++ b/app/Http/Requests/SettingsRequest.php @@ -12,8 +12,12 @@ public function rules(): array return [ 'occupation_id' => ['exists:occupations,id'], + 'channel_id' => ['required', 'string'], + 'enabled' => ['required'], 'color_id' => ['exists:settings_colors,id'], 'effect_id' => ['exists:settings_effects,id'], + 'timezone' => ['string'], + 'locale' => ['string'], 'pronouns' => ['string', 'in:'.$acceptedPronouns], ]; } diff --git a/app/Models/Settings/Color.php b/app/Models/Settings/Color.php index df7c57d..2e6f75e 100644 --- a/app/Models/Settings/Color.php +++ b/app/Models/Settings/Color.php @@ -2,8 +2,10 @@ namespace App\Models\Settings; +use Database\Factories\ColorFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; class Color extends Model { @@ -17,4 +19,14 @@ class Color extends Model 'translation_key', 'hex', ]; + + public function settings(): HasMany + { + return $this->hasMany(Settings::class); + } + + protected static function newFactory(): ColorFactory + { + return ColorFactory::new(); + } } diff --git a/app/Models/Settings/Effect.php b/app/Models/Settings/Effect.php index abc2e77..b9303f5 100644 --- a/app/Models/Settings/Effect.php +++ b/app/Models/Settings/Effect.php @@ -2,8 +2,10 @@ namespace App\Models\Settings; +use Database\Factories\EffectFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; class Effect extends Model { @@ -18,4 +20,14 @@ class Effect extends Model 'class_name', 'hex', ]; + + public function settings(): HasMany + { + return $this->hasMany(Settings::class); + } + + protected static function newFactory(): EffectFactory + { + return EffectFactory::new(); + } } diff --git a/app/Models/Settings/Occupation.php b/app/Models/Settings/Occupation.php index f6d734f..ab1cd12 100644 --- a/app/Models/Settings/Occupation.php +++ b/app/Models/Settings/Occupation.php @@ -2,10 +2,15 @@ namespace App\Models\Settings; +use Database\Factories\OccupationFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; class Occupation extends Model { + use HasFactory; + protected $fillable = ['name', 'slug', 'translation_key']; public function getImageUrlAttribute(): string @@ -16,4 +21,14 @@ public function getImageUrlAttribute(): string protected $appends = [ 'image_url', ]; + + public function settings(): HasMany + { + return $this->hasMany(Settings::class); + } + + protected static function newFactory(): OccupationFactory + { + return OccupationFactory::new(); + } } diff --git a/app/Models/Settings/Settings.php b/app/Models/Settings/Settings.php index 78012d8..a709357 100644 --- a/app/Models/Settings/Settings.php +++ b/app/Models/Settings/Settings.php @@ -3,16 +3,23 @@ namespace App\Models\Settings; use App\Models\User; +use Database\Factories\SettingsFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +/** + * @property string $channel_id + * @property bool $enabled + */ class Settings extends Model { use HasFactory; protected $fillable = [ 'user_id', + 'enabled', + 'channel_id', 'color_id', 'effect_id', 'occupation_id', @@ -24,6 +31,7 @@ class Settings extends Model protected $casts = [ 'is_developer' => 'boolean', + 'enabled' => 'boolean', ]; public function getPronounsAttribute(): array @@ -50,4 +58,9 @@ public function effect(): BelongsTo { return $this->belongsTo(Effect::class); } + + protected static function newFactory(): SettingsFactory + { + return SettingsFactory::new(); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 1f96a44..378d327 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -81,8 +80,8 @@ public function accounts(): HasMany return $this->hasMany(ConnectedAccount::class); } - public function settings(): HasOne + public function settings(): HasMany { - return $this->hasOne(Settings::class); + return $this->hasMany(Settings::class); } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index 35b1ef4..42605f9 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -15,6 +15,7 @@ public function created(User $user): void $user->settings()->create([ 'occupation_id' => 1, // none + 'channel_id' => 'global', 'color_id' => 1, // none 'effect_id' => 1, // none 'pronouns' => 'none', // none diff --git a/database/factories/EffectFactory.php b/database/factories/EffectFactory.php index ed5885c..2edb6b4 100644 --- a/database/factories/EffectFactory.php +++ b/database/factories/EffectFactory.php @@ -18,7 +18,7 @@ public function definition(): array 'name' => $this->faker->name(), 'slug' => $this->faker->slug(), 'translation_key' => $this->faker->word(), - 'class' => $this->faker->word(), + 'class_name' => $this->faker->word(), 'hex' => $this->faker->word(), ]; } diff --git a/database/factories/OccupationFactory.php b/database/factories/OccupationFactory.php new file mode 100644 index 0000000..0a47dbf --- /dev/null +++ b/database/factories/OccupationFactory.php @@ -0,0 +1,20 @@ + $this->faker->name, + 'slug' => $this->faker->name, + 'translation_key' => $this->faker->name, + ]; + } +} diff --git a/database/factories/SettingsFactory.php b/database/factories/SettingsFactory.php index d1156a3..ad98687 100644 --- a/database/factories/SettingsFactory.php +++ b/database/factories/SettingsFactory.php @@ -2,6 +2,8 @@ namespace Database\Factories; +use App\Models\Settings\Color; +use App\Models\Settings\Effect; use App\Models\Settings\Occupation; use App\Models\Settings\Settings; use App\Models\User; @@ -12,15 +14,23 @@ class SettingsFactory extends Factory { protected $model = Settings::class; - public function definition() + public function definition(): array { - return [ - 'created_at' => Carbon::now(), - 'updated_at' => Carbon::now(), - 'pronouns' => $this->faker->word(), + $pronouns = collect(config('extension.pronouns'))->keys()->shuffle()->first(); + return [ 'user_id' => User::factory(), + 'channel_id' => 'global', + 'enabled' => true, + 'color_id' => Color::factory(), + 'effect_id' => Effect::factory(), 'occupation_id' => Occupation::factory(), + 'pronouns' => $pronouns, + 'timezone' => $this->faker->timezone, + 'locale' => $this->faker->locale(), + 'is_developer' => false, + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), ]; } } diff --git a/database/migrations/2024_08_10_210719_create_settings_table.php b/database/migrations/2024_08_10_210719_create_settings_table.php index cd76fb2..c486576 100644 --- a/database/migrations/2024_08_10_210719_create_settings_table.php +++ b/database/migrations/2024_08_10_210719_create_settings_table.php @@ -12,15 +12,17 @@ public function up() $table->id(); $table->foreignId('user_id')->constrained('users'); $table->foreignId('occupation_id')->constrained('occupations'); - $table->string('pronouns'); - $table->string('timezone'); - $table->string('locale'); + $table->boolean('enabled')->default(true); + $table->string('channel_id')->default('global'); + $table->string('pronouns')->nullable(); + $table->string('timezone')->nullable(); + $table->string('locale')->nullable(); $table->boolean('is_developer')->default(false); $table->timestamps(); }); } - public function down() + public function down(): void { Schema::dropIfExists('settings'); } diff --git a/phpunit.xml b/phpunit.xml index 61c031c..98ec861 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,8 +22,8 @@ - - + + diff --git a/routes/api.php b/routes/api.php index 65d1755..4cdbdbc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,6 +13,8 @@ Route::post('/authenticate/{provider}', [OAuthController::class, 'authenticateWithOAuth']); Route::middleware(['auth:sanctum', 'throttle:30,1'])->group(function () { + Route::get('/me/settings', [AuthenticatedUserController::class, 'getSettings']) + ->name('auth.my-settings'); Route::put('/me/update-settings', [AuthenticatedUserController::class, 'putSettings']) ->name('auth.update-settings'); }); diff --git a/tests/Feature/Http/Controllers/Api/V1/AuthenticatedUserControllerTest.php b/tests/Feature/Http/Controllers/Api/V1/AuthenticatedUserControllerTest.php index 3432388..a0d9457 100644 --- a/tests/Feature/Http/Controllers/Api/V1/AuthenticatedUserControllerTest.php +++ b/tests/Feature/Http/Controllers/Api/V1/AuthenticatedUserControllerTest.php @@ -3,23 +3,23 @@ namespace Tests\Feature\Http\Controllers\Api\V1; use App\Clients\Consumer\ConsumerClient; +use App\Models\Settings\Color; +use App\Models\Settings\Effect; +use App\Models\Settings\Occupation; +use App\Models\Settings\Settings; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; class AuthenticatedUserControllerTest extends TestCase { use RefreshDatabase; - protected function setUp(): void - { - parent::setUp(); // TODO: Change the autogenerated stu - } - public function testPutSettings() { // Arrange - $this->artisan('db:seed'); + $user = User::factory()->create(); /** @@ -27,10 +27,9 @@ public function testPutSettings() * the Rust + ScyllaDB (and also Postgres, Redis) just to run the tests, * at the same time I'm not a php dev, so there may well be a better solution for this. */ - $this->partialMock(ConsumerClient::class, function ($mock) use ($user) { + $this->partialMock(ConsumerClient::class, function ($mock) { $mock->shouldReceive('updateUser') ->once() - ->with($user) ->andReturn(true); }); @@ -50,7 +49,11 @@ public function testPutSettings() $payload = [ 'user_id' => $user->id, 'occupation_id' => 2, + 'enabled' => true, + 'channel_id' => 'danielhe4rt', 'pronouns' => 'she-her', + 'effect_id' => 1, + 'color_id' => 1, ]; // Act @@ -71,4 +74,45 @@ public function testPutSettings() 'pronouns' => 'she-her', ]); } + + #[DataProvider('settingsDataProvider')] + public function testGetSettings(?string $payload, int $count) + { + // Arrange + $user = User::factory()->create(); + + if ($payload) { + Settings::factory()->create([ + 'user_id' => $user->id, + 'channel_id' => $payload, + 'occupation_id' => Occupation::factory(), + 'color_id' => Color::factory(), + 'effect_id' => Effect::factory(), + ]); + } + + // Act + $response = $this + ->actingAs($user) + ->get(route('auth.my-settings', ['channel_id' => $payload])); + + // Assert + $response + ->assertOk() + ->assertJsonCount($count, 'data'); + } + + public static function settingsDataProvider() + { + return [ + 'only_global' => [ + 'payload' => null, + 'count' => 1, + ], + 'with_channel' => [ + 'payload' => 'danielhe4rt', + 'count' => 2, + ], + ]; + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index fe1ffc2..b124225 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,5 @@ abstract class TestCase extends BaseTestCase { - // + public $seed = true; }