user = User::factory()->create(); $this->workspace = Workspace::factory()->create(); $this->workspace->users()->attach($this->user->id, [ 'role' => 'owner', 'is_default' => true, ]); // Use existing seeded package $this->package = Package::where('code', 'creator')->first(); // Create test coupons $this->percentCoupon = Coupon::create([ 'code' => 'SAVE20', 'name' => '20% Off', 'type' => 'percentage', 'value' => 20.00, 'applies_to' => 'all', 'is_active' => true, 'max_uses' => 100, 'max_uses_per_workspace' => 1, 'used_count' => 0, ]); $this->fixedCoupon = Coupon::create([ 'code' => 'FLAT10', 'name' => '£10 Off', 'type' => 'fixed_amount', 'value' => 10.00, 'applies_to' => 'all', 'is_active' => true, 'max_uses_per_workspace' => 1, ]); $this->service = app(CouponService::class); }); describe('CouponService', function () { describe('sanitiseCode() method', function () { it('trims whitespace from coupon codes', function () { $result = $this->service->sanitiseCode(' SAVE20 '); expect($result)->toBe('SAVE20'); }); it('converts lowercase to uppercase', function () { $result = $this->service->sanitiseCode('save20'); expect($result)->toBe('SAVE20'); }); it('handles mixed case codes', function () { $result = $this->service->sanitiseCode('SaVe20'); expect($result)->toBe('SAVE20'); }); it('allows hyphens in codes', function () { $result = $this->service->sanitiseCode('SAVE-20-NOW'); expect($result)->toBe('SAVE-20-NOW'); }); it('allows underscores in codes', function () { $result = $this->service->sanitiseCode('SAVE_20_NOW'); expect($result)->toBe('SAVE_20_NOW'); }); it('rejects codes shorter than minimum length', function () { $result = $this->service->sanitiseCode('AB'); expect($result)->toBeNull(); }); it('accepts codes at minimum length', function () { $result = $this->service->sanitiseCode('ABC'); expect($result)->toBe('ABC'); }); it('rejects codes longer than maximum length', function () { $longCode = str_repeat('A', 51); $result = $this->service->sanitiseCode($longCode); expect($result)->toBeNull(); }); it('accepts codes at maximum length', function () { $maxCode = str_repeat('A', 50); $result = $this->service->sanitiseCode($maxCode); expect($result)->toBe($maxCode); }); it('rejects codes with invalid characters', function () { $invalidCodes = [ 'SAVE@20', // @ symbol 'SAVE 20', // space (after trim) 'SAVE#20', // hash 'SAVE!20', // exclamation 'SAVE$20', // dollar 'SAVE%20', // percent 'SAVE&20', // ampersand 'SAVE*20', // asterisk 'SAVE.20', // period "SAVE'20", // single quote 'SAVE"20', // double quote 'SAVE;20', // semicolon (SQL injection attempt) "SAVE'--20", // SQL injection attempt 'SAVE