Compare commits

..

No commits in common. "3cdc2678093b6efb260ae434260dbb699396b99e" and "d81c1b99f1611ac05ab4be63fd36acec8c56ff5e" have entirely different histories.

10 changed files with 63 additions and 227 deletions

View file

@ -20,14 +20,6 @@ class Device extends Model
protected $guarded = ['id']; protected $guarded = ['id'];
/**
* Set the MAC address attribute, normalizing to uppercase.
*/
public function setMacAddressAttribute(?string $value): void
{
$this->attributes['mac_address'] = $value ? mb_strtoupper($value) : null;
}
protected $casts = [ protected $casts = [
'battery_notification_sent' => 'boolean', 'battery_notification_sent' => 'boolean',
'proxy_cloud' => 'boolean', 'proxy_cloud' => 'boolean',

View file

@ -37,32 +37,21 @@ class Playlist extends Model
return false; return false;
} }
// Get user's timezone or fall back to app timezone // Check weekday
$timezone = $this->device->user->timezone ?? config('app.timezone'); if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) {
$now = now($timezone);
// Check weekday (using timezone-aware time)
if ($this->weekdays !== null && ! in_array($now->dayOfWeek, $this->weekdays)) {
return false; return false;
} }
if ($this->active_from !== null && $this->active_until !== null) { if ($this->active_from !== null && $this->active_until !== null) {
// Create timezone-aware datetime objects for active_from and active_until $now = now();
$activeFrom = $now->copy()
->setTimeFrom($this->active_from)
->timezone($timezone);
$activeUntil = $now->copy()
->setTimeFrom($this->active_until)
->timezone($timezone);
// Handle time ranges that span across midnight // Handle time ranges that span across midnight
if ($activeFrom > $activeUntil) { if ($this->active_from > $this->active_until) {
// Time range spans midnight (e.g., 09:01 to 03:58) // Time range spans midnight (e.g., 09:01 to 03:58)
if ($now >= $activeFrom || $now <= $activeUntil) { if ($now >= $this->active_from || $now <= $this->active_until) {
return true; return true;
} }
} elseif ($now >= $activeFrom && $now <= $activeUntil) { } elseif ($now >= $this->active_from && $now <= $this->active_until) {
return true; return true;
} }

View file

@ -55,13 +55,6 @@ class Plugin extends Model
$model->uuid = Str::uuid(); $model->uuid = Str::uuid();
} }
}); });
static::updating(function ($model): void {
// Reset image cache when markup changes
if ($model->isDirty('render_markup')) {
$model->current_image = null;
}
});
} }
public function user() public function user()

View file

@ -34,7 +34,7 @@ class IcalResponseParser implements ResponseParser
$filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool { $filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool {
$startDate = $this->asCarbon($event['DTSTART'] ?? null); $startDate = $this->asCarbon($event['DTSTART'] ?? null);
if (! $startDate instanceof Carbon) { if (!$startDate instanceof \Carbon\Carbon) {
return false; return false;
} }

100
composer.lock generated
View file

@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.369.4", "version": "3.369.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "2aa1ef195e90140d733382e4341732ce113024f5" "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3",
"reference": "2aa1ef195e90140d733382e4341732ce113024f5", "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -85,7 +85,7 @@
"mtdowling/jmespath.php": "^2.8.0", "mtdowling/jmespath.php": "^2.8.0",
"php": ">=8.1", "php": ">=8.1",
"psr/http-message": "^1.0 || ^2.0", "psr/http-message": "^1.0 || ^2.0",
"symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0"
}, },
"require-dev": { "require-dev": {
"andrewsville/php-token-reflection": "^1.4", "andrewsville/php-token-reflection": "^1.4",
@ -153,9 +153,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.4" "source": "https://github.com/aws/aws-sdk-php/tree/3.369.1"
}, },
"time": "2025-12-29T19:07:47+00:00" "time": "2025-12-22T19:13:21+00:00"
}, },
{ {
"name": "bnussbau/laravel-trmnl-blade", "name": "bnussbau/laravel-trmnl-blade",
@ -950,24 +950,24 @@
}, },
{ {
"name": "graham-campbell/result-type", "name": "graham-campbell/result-type",
"version": "v1.1.4", "version": "v1.1.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git", "url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" "reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5" "phpoption/phpoption": "^1.9.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -996,7 +996,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues", "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
}, },
"funding": [ "funding": [
{ {
@ -1008,7 +1008,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-27T19:43:20+00:00" "time": "2024-07-20T21:45:45+00:00"
}, },
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
@ -1617,16 +1617,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.44.0", "version": "v12.43.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33" "reference": "195b893593a9298edee177c0844132ebaa02102f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", "url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33", "reference": "195b893593a9298edee177c0844132ebaa02102f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1835,7 +1835,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2025-12-23T15:29:43+00:00" "time": "2025-12-16T18:53:08+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
@ -3885,16 +3885,16 @@
}, },
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.5", "version": "1.9.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/schmittjoh/php-option.git", "url": "https://github.com/schmittjoh/php-option.git",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be" "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3944,7 +3944,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/schmittjoh/php-option/issues", "issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.5" "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
}, },
"funding": [ "funding": [
{ {
@ -3956,7 +3956,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-27T19:41:33+00:00" "time": "2025-08-21T11:53:16+00:00"
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
@ -7723,26 +7723,26 @@
}, },
{ {
"name": "vlucas/phpdotenv", "name": "vlucas/phpdotenv",
"version": "v5.6.3", "version": "v5.6.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/vlucas/phpdotenv.git", "url": "https://github.com/vlucas/phpdotenv.git",
"reference": "955e7815d677a3eaa7075231212f2110983adecc" "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"reference": "955e7815d677a3eaa7075231212f2110983adecc", "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-pcre": "*", "ext-pcre": "*",
"graham-campbell/result-type": "^1.1.4", "graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.5", "phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.26", "symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.26", "symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.26" "symfony/polyfill-php80": "^1.24"
}, },
"require-dev": { "require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2", "bamarni/composer-bin-plugin": "^1.8.2",
@ -7791,7 +7791,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/vlucas/phpdotenv/issues", "issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
}, },
"funding": [ "funding": [
{ {
@ -7803,7 +7803,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-27T19:49:13+00:00" "time": "2025-04-30T23:37:27+00:00"
}, },
{ {
"name": "voku/portable-ascii", "name": "voku/portable-ascii",
@ -10122,16 +10122,16 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "12.5.2", "version": "12.5.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57",
"reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10146,7 +10146,7 @@
"sebastian/environment": "^8.0.3", "sebastian/environment": "^8.0.3",
"sebastian/lines-of-code": "^4.0", "sebastian/lines-of-code": "^4.0",
"sebastian/version": "^6.0", "sebastian/version": "^6.0",
"theseer/tokenizer": "^2.0.1" "theseer/tokenizer": "^2.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^12.5.1" "phpunit/phpunit": "^12.5.1"
@ -10187,7 +10187,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.1"
}, },
"funding": [ "funding": [
{ {
@ -10207,7 +10207,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-24T07:03:04+00:00" "time": "2025-12-08T07:17:58+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@ -10561,16 +10561,16 @@
}, },
{ {
"name": "rector/rector", "name": "rector/rector",
"version": "2.3.0", "version": "2.2.14",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/rectorphp/rector.git", "url": "https://github.com/rectorphp/rector.git",
"reference": "f7166355dcf47482f27be59169b0825995f51c7d" "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", "url": "https://api.github.com/repos/rectorphp/rector/zipball/6d56bb0e94d4df4f57a78610550ac76ab403657d",
"reference": "f7166355dcf47482f27be59169b0825995f51c7d", "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -10609,7 +10609,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/rectorphp/rector/issues", "issues": "https://github.com/rectorphp/rector/issues",
"source": "https://github.com/rectorphp/rector/tree/2.3.0" "source": "https://github.com/rectorphp/rector/tree/2.2.14"
}, },
"funding": [ "funding": [
{ {
@ -10617,7 +10617,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-12-25T22:00:18+00:00" "time": "2025-12-09T10:57:55+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",

View file

@ -20,7 +20,7 @@ class DevicePaletteFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'id' => 'test-'.$this->faker->unique()->slug(), 'id' => 'test-' . $this->faker->unique()->slug(),
'name' => $this->faker->words(3, true), 'name' => $this->faker->words(3, true),
'grays' => $this->faker->randomElement([2, 4, 16, 256]), 'grays' => $this->faker->randomElement([2, 4, 16, 256]),
'colors' => $this->faker->optional()->passthrough([ 'colors' => $this->faker->optional()->passthrough([

View file

@ -18,7 +18,7 @@ use Illuminate\Support\Str;
Route::get('/display', function (Request $request) { Route::get('/display', function (Request $request) {
$mac_address = $request->header('id'); $mac_address = $request->header('id');
$access_token = $request->header('access-token'); $access_token = $request->header('access-token');
$device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) $device = Device::where('mac_address', $mac_address)
->where('api_key', $access_token) ->where('api_key', $access_token)
->first(); ->first();
@ -29,7 +29,7 @@ Route::get('/display', function (Request $request) {
if ($auto_assign_user) { if ($auto_assign_user) {
// Create a new device and assign it to this user // Create a new device and assign it to this user
$device = Device::create([ $device = Device::create([
'mac_address' => mb_strtoupper($mac_address ?? ''), 'mac_address' => $mac_address,
'api_key' => $access_token, 'api_key' => $access_token,
'user_id' => $auto_assign_user->id, 'user_id' => $auto_assign_user->id,
'name' => "{$auto_assign_user->name}'s TRMNL", 'name' => "{$auto_assign_user->name}'s TRMNL",
@ -204,7 +204,7 @@ Route::get('/setup', function (Request $request) {
], 404); ], 404);
} }
$device = Device::where('mac_address', mb_strtoupper($mac_address))->first(); $device = Device::where('mac_address', $mac_address)->first();
if (! $device) { if (! $device) {
// Check if there's a user with assign_new_devices enabled // Check if there's a user with assign_new_devices enabled
@ -219,7 +219,7 @@ Route::get('/setup', function (Request $request) {
// Create a new device and assign it to this user // Create a new device and assign it to this user
$device = Device::create([ $device = Device::create([
'mac_address' => mb_strtoupper($mac_address), 'mac_address' => $mac_address,
'api_key' => Str::random(22), 'api_key' => Str::random(22),
'user_id' => $auto_assign_user->id, 'user_id' => $auto_assign_user->id,
'name' => "{$auto_assign_user->name}'s TRMNL", 'name' => "{$auto_assign_user->name}'s TRMNL",
@ -345,7 +345,7 @@ Route::post('/display/update', function (Request $request) {
Route::post('/screens', function (Request $request) { Route::post('/screens', function (Request $request) {
$mac_address = $request->header('id'); $mac_address = $request->header('id');
$access_token = $request->header('access-token'); $access_token = $request->header('access-token');
$device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) $device = Device::where('mac_address', $mac_address)
->where('api_key', $access_token) ->where('api_key', $access_token)
->first(); ->first();

View file

@ -954,72 +954,3 @@ test('setup endpoint handles non-existent device model gracefully', function ():
expect($device)->not->toBeNull() expect($device)->not->toBeNull()
->and($device->device_model_id)->toBeNull(); ->and($device->device_model_id)->toBeNull();
}); });
test('setup endpoint matches MAC address case-insensitively', function (): void {
// Create device with lowercase MAC address
$device = Device::factory()->create([
'mac_address' => 'a1:b2:c3:d4:e5:f6',
'api_key' => 'test-api-key',
'friendly_id' => 'test-device',
]);
// Request with uppercase MAC address should still match
$response = $this->withHeaders([
'id' => 'A1:B2:C3:D4:E5:F6',
])->get('/api/setup');
$response->assertOk()
->assertJson([
'status' => 200,
'api_key' => 'test-api-key',
'friendly_id' => 'test-device',
'message' => 'Welcome to TRMNL BYOS',
]);
});
test('display endpoint matches MAC address case-insensitively', function (): void {
// Create device with lowercase MAC address
$device = Device::factory()->create([
'mac_address' => 'a1:b2:c3:d4:e5:f6',
'api_key' => 'test-api-key',
'current_screen_image' => 'test-image',
]);
// Request with uppercase MAC address should still match
$response = $this->withHeaders([
'id' => 'A1:B2:C3:D4:E5:F6',
'access-token' => $device->api_key,
'rssi' => -70,
'battery_voltage' => 3.8,
'fw-version' => '1.0.0',
])->get('/api/display');
$response->assertOk()
->assertJson([
'status' => '0',
'filename' => 'test-image.bmp',
]);
});
test('screens endpoint matches MAC address case-insensitively', function (): void {
Queue::fake();
// Create device with uppercase MAC address
$device = Device::factory()->create([
'mac_address' => 'A1:B2:C3:D4:E5:F6',
'api_key' => 'test-api-key',
]);
// Request with lowercase MAC address should still match
$response = $this->withHeaders([
'id' => 'a1:b2:c3:d4:e5:f6',
'access-token' => $device->api_key,
])->post('/api/screens', [
'image' => [
'content' => '<div>Test content</div>',
],
]);
$response->assertOk();
Queue::assertPushed(GenerateScreenJob::class);
});

View file

@ -324,30 +324,6 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void
expect($plugin->current_image)->toBe('test-uuid'); expect($plugin->current_image)->toBe('test-uuid');
}); });
it('cache is reset when plugin markup changes', function (): void {
// Create a plugin with cached image
$plugin = App\Models\Plugin::factory()->create([
'current_image' => 'cached-uuid',
'render_markup' => '<div>Original markup</div>',
]);
// Create devices with standard dimensions (cacheable)
Device::factory()->count(2)->create([
'width' => 800,
'height' => 480,
'rotate' => 0,
]);
// Update the plugin markup
$plugin->update([
'render_markup' => '<div>Updated markup</div>',
]);
// Assert cache was reset when markup changed
$plugin->refresh();
expect($plugin->current_image)->toBeNull();
});
it('determines correct image format from device model', function (): void { it('determines correct image format from device model', function (): void {
// Test BMP format detection // Test BMP format detection
$bmpModel = DeviceModel::factory()->create([ $bmpModel = DeviceModel::factory()->create([

View file

@ -130,48 +130,3 @@ test('playlist isActiveNow handles normal time ranges correctly', function (): v
Carbon::setTestNow(Carbon::create(2024, 1, 1, 20, 0, 0)); Carbon::setTestNow(Carbon::create(2024, 1, 1, 20, 0, 0));
expect($playlist->isActiveNow())->toBeFalse(); expect($playlist->isActiveNow())->toBeFalse();
}); });
test('playlist scheduling respects user timezone preference', function (): void {
// Create a user with a timezone that's UTC+1 (e.g., Europe/Berlin)
// This simulates the bug where setting 00:15 doesn't work until one hour later
$user = User::factory()->create([
'timezone' => 'Europe/Berlin', // UTC+1 in winter, UTC+2 in summer
]);
$device = Device::factory()->create(['user_id' => $user->id]);
// Create a playlist that should be active from 00:15 to 01:00 in the user's timezone
$playlist = Playlist::factory()->create([
'device_id' => $device->id,
'is_active' => true,
'active_from' => '00:15',
'active_until' => '01:00',
'weekdays' => null,
]);
// Set test time to 00:15 in the user's timezone (Europe/Berlin)
// In January, Europe/Berlin is UTC+1, so 00:15 Berlin time = 23:15 UTC the previous day
// But Carbon::setTestNow uses UTC by default, so we need to set it to the UTC equivalent
// For January 1, 2024 at 00:15 Berlin time (UTC+1), that's December 31, 2023 at 23:15 UTC
$berlinTime = Carbon::create(2024, 1, 1, 0, 15, 0, 'Europe/Berlin');
Carbon::setTestNow($berlinTime->utc());
// The playlist should be active at 00:15 in the user's timezone
// This test should pass after the fix, but will fail with the current bug
expect($playlist->isActiveNow())->toBeTrue();
// Test at 00:30 in user's timezone - should still be active
$berlinTime = Carbon::create(2024, 1, 1, 0, 30, 0, 'Europe/Berlin');
Carbon::setTestNow($berlinTime->utc());
expect($playlist->isActiveNow())->toBeTrue();
// Test at 01:15 in user's timezone - should NOT be active (past the end time)
$berlinTime = Carbon::create(2024, 1, 1, 1, 15, 0, 'Europe/Berlin');
Carbon::setTestNow($berlinTime->utc());
expect($playlist->isActiveNow())->toBeFalse();
// Test at 00:10 in user's timezone - should NOT be active (before start time)
$berlinTime = Carbon::create(2024, 1, 1, 0, 10, 0, 'Europe/Berlin');
Carbon::setTestNow($berlinTime->utc());
expect($playlist->isActiveNow())->toBeFalse();
});