Compare commits

...

5 commits

Author SHA1 Message Date
Benjamin Nussbaum
3cdc267809 chore: pint
Some checks are pending
tests / ci (push) Waiting to run
2025-12-29 23:08:52 +01:00
Benjamin Nussbaum
1298814521 fix(#136): mac address matching is case senstive 2025-12-29 23:07:21 +01:00
Benjamin Nussbaum
a5cb38421e fix(#131): invalidate cache when updating recipe markup 2025-12-29 22:24:32 +01:00
Benjamin Nussbaum
e6d66af298 fix(#135): use user configured timezone in Playlists 2025-12-29 22:16:29 +01:00
Benjamin Nussbaum
d4b5cf99d5 chore: update dependencies 2025-12-29 22:05:20 +01:00
10 changed files with 227 additions and 63 deletions

View file

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

View file

@ -55,6 +55,13 @@ 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\Carbon) { if (! $startDate instanceof 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.1", "version": "3.369.4",
"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": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3" "reference": "2aa1ef195e90140d733382e4341732ce113024f5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5",
"reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", "reference": "2aa1ef195e90140d733382e4341732ce113024f5",
"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": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" "symfony/filesystem": "^v5.4.45 || ^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.1" "source": "https://github.com/aws/aws-sdk-php/tree/3.369.4"
}, },
"time": "2025-12-22T19:13:21+00:00" "time": "2025-12-29T19:07:47+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.3", "version": "v1.1.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git", "url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945" "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945", "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3" "phpoption/phpoption": "^1.9.5"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
}, },
"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.3" "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
}, },
"funding": [ "funding": [
{ {
@ -1008,7 +1008,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-07-20T21:45:45+00:00" "time": "2025-12-27T19:43:20+00:00"
}, },
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
@ -1617,16 +1617,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.43.1", "version": "v12.44.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "195b893593a9298edee177c0844132ebaa02102f" "reference": "592bbf1c036042958332eb98e3e8131b29102f33"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f", "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33",
"reference": "195b893593a9298edee177c0844132ebaa02102f", "reference": "592bbf1c036042958332eb98e3e8131b29102f33",
"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-16T18:53:08+00:00" "time": "2025-12-23T15:29:43+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
@ -3885,16 +3885,16 @@
}, },
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.4", "version": "1.9.5",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/schmittjoh/php-option.git", "url": "https://github.com/schmittjoh/php-option.git",
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" "reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
"reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", "reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
"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.4" "source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
}, },
"funding": [ "funding": [
{ {
@ -3956,7 +3956,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-21T11:53:16+00:00" "time": "2025-12-27T19:41:33+00:00"
}, },
{ {
"name": "phpseclib/phpseclib", "name": "phpseclib/phpseclib",
@ -7723,26 +7723,26 @@
}, },
{ {
"name": "vlucas/phpdotenv", "name": "vlucas/phpdotenv",
"version": "v5.6.2", "version": "v5.6.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/vlucas/phpdotenv.git", "url": "https://github.com/vlucas/phpdotenv.git",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" "reference": "955e7815d677a3eaa7075231212f2110983adecc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
"reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "reference": "955e7815d677a3eaa7075231212f2110983adecc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-pcre": "*", "ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3", "graham-campbell/result-type": "^1.1.4",
"php": "^7.2.5 || ^8.0", "php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3", "phpoption/phpoption": "^1.9.5",
"symfony/polyfill-ctype": "^1.24", "symfony/polyfill-ctype": "^1.26",
"symfony/polyfill-mbstring": "^1.24", "symfony/polyfill-mbstring": "^1.26",
"symfony/polyfill-php80": "^1.24" "symfony/polyfill-php80": "^1.26"
}, },
"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.2" "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
}, },
"funding": [ "funding": [
{ {
@ -7803,7 +7803,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-04-30T23:37:27+00:00" "time": "2025-12-27T19:49:13+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.1", "version": "12.5.2",
"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": "c467c59a4f6e04b942be422844e7a6352fa01b57" "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b",
"reference": "c467c59a4f6e04b942be422844e7a6352fa01b57", "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b",
"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" "theseer/tokenizer": "^2.0.1"
}, },
"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.1" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2"
}, },
"funding": [ "funding": [
{ {
@ -10207,7 +10207,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-12-08T07:17:58+00:00" "time": "2025-12-24T07:03:04+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@ -10561,16 +10561,16 @@
}, },
{ {
"name": "rector/rector", "name": "rector/rector",
"version": "2.2.14", "version": "2.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/rectorphp/rector.git", "url": "https://github.com/rectorphp/rector.git",
"reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d" "reference": "f7166355dcf47482f27be59169b0825995f51c7d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/rectorphp/rector/zipball/6d56bb0e94d4df4f57a78610550ac76ab403657d", "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d",
"reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d", "reference": "f7166355dcf47482f27be59169b0825995f51c7d",
"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.2.14" "source": "https://github.com/rectorphp/rector/tree/2.3.0"
}, },
"funding": [ "funding": [
{ {
@ -10617,7 +10617,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-12-09T10:57:55+00:00" "time": "2025-12-25T22:00:18+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', $mac_address) $device = Device::where('mac_address', mb_strtoupper($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' => $mac_address, 'mac_address' => mb_strtoupper($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', $mac_address)->first(); $device = Device::where('mac_address', mb_strtoupper($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' => $mac_address, 'mac_address' => mb_strtoupper($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', $mac_address) $device = Device::where('mac_address', mb_strtoupper($mac_address ?? ''))
->where('api_key', $access_token) ->where('api_key', $access_token)
->first(); ->first();

View file

@ -954,3 +954,72 @@ 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,6 +324,30 @@ 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,3 +130,48 @@ 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();
});