import 'package:platform_contracts/contracts.dart'; import 'package:platform_container/platform_container.dart'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; import 'package:mockito/annotations.dart'; import 'laravel_container_test.mocks.dart'; // Test classes abstract class PaymentGateway {} class StripeGateway implements PaymentGateway {} class PayPalGateway implements PaymentGateway {} class Service {} class SpecialService extends Service {} class Client { final Service service; Client(this.service); } @GenerateMocks([ ReflectorContract, ReflectedClassContract, ReflectedInstanceContract, ReflectedFunctionContract, ReflectedParameterContract, ReflectedTypeContract ]) void main() { late IlluminateContainer container; late MockReflectorContract reflector; late MockReflectedClassContract reflectedClass; late MockReflectedInstanceContract reflectedInstance; late MockReflectedFunctionContract reflectedFunction; late MockReflectedParameterContract reflectedParameter; late MockReflectedTypeContract reflectedType; setUp(() { reflector = MockReflectorContract(); reflectedClass = MockReflectedClassContract(); reflectedInstance = MockReflectedInstanceContract(); reflectedFunction = MockReflectedFunctionContract(); reflectedParameter = MockReflectedParameterContract(); reflectedType = MockReflectedTypeContract(); container = IlluminateContainer(reflector); // Setup default reflection behavior when(reflector.reflectClass(any)).thenReturn(null); when(reflector.reflectFunction(any)).thenReturn(null); }); group('Laravel Container', () { test('contextual binding with when/needs/give', () { // When we set up a contextual binding container.when(Client).needs().give(SpecialService()); // Then the contextual binding should be used when(reflector.reflectClass(Client)).thenReturn(reflectedClass); when(reflectedClass.newInstance('', [])).thenReturn(reflectedInstance); when(reflectedInstance.reflectee).thenReturn(Client(SpecialService())); final client = container.make(); expect(client.service, isA()); }); test('method injection with call', () { void method(Service service) {} // Setup reflection mocks when(reflector.reflectFunction(method)).thenReturn(reflectedFunction); when(reflectedFunction.parameters).thenReturn([reflectedParameter]); when(reflectedParameter.type).thenReturn(reflectedType); when(reflectedType.reflectedType).thenReturn(Service); // Register service final service = Service(); container.registerSingleton(service); // When we call the method through the container container.call(method); // Then dependencies should be injected verify(reflector.reflectFunction(method)).called(1); }); test('resolution hooks', () { // Given some resolution hooks final resolving = []; final testValue = 'test'; // Register hooks with explicit type parameters void beforeCallback(ContainerContract c, String? i) => resolving.add('before'); void resolvingCallback(ContainerContract c, String i) => resolving.add('resolving'); void afterCallback(ContainerContract c, String i) => resolving.add('after'); container.beforeResolving(beforeCallback); container.resolving(resolvingCallback); container.afterResolving(afterCallback); // Register a value to resolve container.registerSingleton(testValue); // When we resolve a type container.make(); // Then hooks should be called in order expect(resolving, equals(['before', 'resolving', 'after'])); }); test('method binding', () { // Setup reflection mocks when(reflector.reflectFunction(any)).thenReturn(reflectedFunction); when(reflectedFunction.parameters).thenReturn([]); // Register a method String boundMethod() => 'bound'; container.bindMethod('test', boundMethod); // Call a test method String testMethod() => 'test'; final result = container.call(testMethod); // Verify the method was called verify(reflector.reflectFunction(any)).called(1); expect(result, equals('test')); }); test('tagged bindings', () { // Create gateway instances final stripeGateway = StripeGateway(); final paypalGateway = PayPalGateway(); // Register factories for concrete types container.registerFactory((c) => stripeGateway); container.registerFactory((c) => paypalGateway); // Register types in order container.tag([StripeGateway, PayPalGateway], 'gateways'); // When we resolve tagged bindings final gateways = container.tagged('gateways'); // Then all tagged instances should be returned in order expect(gateways.length, equals(2)); expect(gateways[0], same(stripeGateway)); expect(gateways[1], same(paypalGateway)); }); test('shared bindings', () { // Given a shared binding container.bind((c) => 'test', shared: true); // When we resolve multiple times final first = container.make(); final second = container.make(); // Then the same instance should be returned expect(identical(first, second), isTrue); }); test('child container inherits bindings', () { // Given a parent binding container.bind((c) => 'parent'); // When we create a child container final child = container.createChild(); // Then the child should inherit bindings expect(child.make(), equals('parent')); }); test('child container can override bindings', () { // Given a parent binding container.registerFactory((c) => 'parent'); // When we override in child final child = container.createChild(); child.registerFactory((c) => 'child'); // Then child should use its own binding expect(child.make(), equals('child')); expect(container.make(), equals('parent')); }); test('scoped bindings', () { // Given a scoped binding var counter = 0; container.scoped((c) => 'scoped${counter++}'); // When we resolve multiple times final first = container.make(); final second = container.make(); // Then the same instance should be returned expect(first, equals(second)); // When we forget scoped instances container.forgetScopedInstances(); // Then a new instance should be created final third = container.make(); expect(third, isNot(equals(second))); }); test('type aliases', () { // Given a binding and an alias container.bind((c) => StripeGateway()); container.alias(PaymentGateway, StripeGateway); // When we check if it's an alias final isAlias = container.isAlias('StripeGateway'); // Then it should be true expect(isAlias, isTrue); // And the alias should resolve to the original type expect(container.getAlias(StripeGateway), equals(PaymentGateway)); }); test('extending instances', () { print('Starting extending instances test'); // Given a binding and an extender var extended = false; print('Setting up binding and extender'); container.bind((c) => Service()); container.extend(Service, (instance) { print('Extender called'); extended = true; }); print('Making instance'); // When we make an instance container.make(); print('Extended value: $extended'); // Then the extender should be called expect(extended, isTrue); // And we can get the extenders expect(container.getExtenders(Service).length, equals(1)); // And we can forget them container.forgetExtenders(Service); expect(container.getExtenders(Service).length, equals(0)); }); test('rebinding callbacks', () { // Given a binding and a rebind callback var rebound = false; container.bind((c) => Service()); container.rebinding(Service, (c, i) => rebound = true); // When we refresh an instance final target = {'method': (Service s) {}}; container.refresh(Service, target, 'method'); // Then the callback should be called expect(rebound, isTrue); }); test('container flushing', () { // Given some bindings and instances container.bind((c) => Service()); container.registerSingleton(PayPalGateway()); container.registerNamedSingleton('test', 'value'); container.tag([Service], 'services'); container.when(Client).needs().give(SpecialService()); container.bindMethod('test', () {}); container.alias(PaymentGateway, PayPalGateway); container.extend(Service, (i) {}); container.rebinding(Service, (c, i) {}); container.scoped((c) => 'scoped'); container.beforeResolving((c, i) {}); // When we flush the container container.flush(); // Then everything should be cleared expect(container.has(), isFalse); expect(container.hasNamed('test'), isFalse); expect(() => container.tagged('services'), throwsStateError); expect(container.getExtenders(Service).length, equals(0)); }); }); }