'p95n', 'order' => 'desc'])) ->toBe('ORDER BY rs.p95n DESC'); }); test('every legal measure value is accepted', function () { foreach (['average', 'peak', 'p25n', 'p50n', 'p75n', 'p90n', 'p95n', 'sum'] as $measure) { expect(get_dsstats_order_string(['measure' => $measure, 'order' => 'asc'])) ->toBe('ORDER BY rs.' . $measure . ' ASC'); } }); test('order is case-insensitive', function () { expect(get_dsstats_order_string(['measure' => 'sum', 'order' => 'DESC']))->toBe('ORDER BY rs.sum DESC'); expect(get_dsstats_order_string(['measure' => 'sum', 'order' => 'Desc']))->toBe('ORDER BY rs.sum DESC'); expect(get_dsstats_order_string(['measure' => 'sum', 'order' => 'desc']))->toBe('ORDER BY rs.sum DESC'); expect(get_dsstats_order_string(['measure' => 'sum', 'order' => 'ASC']))->toBe('ORDER BY rs.sum ASC'); }); test('unrecognised measure falls back to average instead of reaching SQL', function () { $clause = get_dsstats_order_string(['measure' => 'average; DROP TABLE user_auth-- ', 'order' => 'asc']); expect($clause)->toBe('ORDER BY rs.average ASC') ->and($clause)->not->toContain(';') ->and($clause)->not->toContain('--'); }); test('UNION SELECT payload in measure is rejected', function () { $clause = get_dsstats_order_string([ 'measure' => '1) UNION SELECT password FROM user_auth-- ', 'order' => 'asc', ]); expect($clause)->toBe('ORDER BY rs.average ASC') ->and($clause)->not->toContain('UNION') ->and($clause)->not->toContain('user_auth'); }); test('subquery payload in measure is rejected', function () { $clause = get_dsstats_order_string([ 'measure' => '(SELECT password FROM user_auth LIMIT 1)', 'order' => 'asc', ]); expect($clause)->toBe('ORDER BY rs.average ASC') ->and($clause)->not->toContain('SELECT'); }); test('SQL payload in order falls back to ASC', function () { expect(get_dsstats_order_string(['measure' => 'sum', 'order' => 'asc; DROP TABLE user_auth-- '])) ->toBe('ORDER BY rs.sum ASC'); }); test('missing keys default to a safe clause', function () { expect(get_dsstats_order_string([]))->toBe('ORDER BY rs.average ASC'); });