Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Refactor permissions logic and remove OCMock from tests
  • Loading branch information
mchudy committed Dec 30, 2024
commit 0babbd5fd42c93038ad591007f8f1d29b7fe05cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,68 @@
#endif
@import AVFoundation;
@import XCTest;
#import <OCMock/OCMock.h>
#import "CameraTestUtils.h"
#import "FLTPermissionService.h"
#import "FLTCameraPermissionManager.h"

@interface CameraPermissionTests : XCTestCase
@interface MockPermissionService : NSObject <FLTPermissionService>
@property (nonatomic, assign) AVAuthorizationStatus cameraAuthorizationStatusStub;
@property (nonatomic, assign) AVAuthorizationStatus audioAuthorizationStatusStub;

@property (nonatomic, assign) BOOL cameraGrantAccessStub;
@property (nonatomic, assign) BOOL audioGrantAccessStub;
@end

@implementation MockPermissionService
- (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType {
if (mediaType == AVMediaTypeVideo) {
return self.cameraAuthorizationStatusStub;
} else if (mediaType == AVMediaTypeAudio) {
return self.audioAuthorizationStatusStub;
}
@throw [NSException exceptionWithName:@"UnexpectedMediaType"
reason:@"Unexpected media type was used"
userInfo:nil];
}

- (void)requestAccessForMediaType:(AVMediaType)mediaType
completionHandler:(void (^)(BOOL granted))handler {
if (mediaType == AVMediaTypeVideo) {
handler(self.cameraGrantAccessStub);
} else if (mediaType == AVMediaTypeAudio) {
handler(self.audioGrantAccessStub);
}
}
@end

@interface CameraPermissionTests : XCTestCase
@property (nonatomic, strong) FLTCameraPermissionManager *permissionManager;
@property (nonatomic, strong) MockPermissionService *mockService;
@end

@implementation CameraPermissionTests

- (void)setUp {
[super setUp];
self.mockService = [[MockPermissionService alloc] init];
self.permissionManager = [[FLTCameraPermissionManager alloc]
initWithPermissionService:self.mockService];
}

#pragma mark - camera permissions

- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
- (void)testRequestCameraPermission_completeWithoutErrorIfPreviouslyAuthorized {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Must copmlete without error if camera access was previously authorized."];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusAuthorized);
self.mockService.cameraAuthorizationStatusStub = AVAuthorizationStatusAuthorized;

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
[self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
if (error == nil) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied {
Expand All @@ -45,14 +82,13 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied {
@"Settings to enable camera access."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusDenied);
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
self.mockService.cameraAuthorizationStatusStub = AVAuthorizationStatusDenied;

[self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

Expand All @@ -63,37 +99,30 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted {
message:@"Camera access is restricted. "
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusRestricted);
self.mockService.cameraAuthorizationStatusStub = AVAuthorizationStatusRestricted;

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
[self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess {
XCTestExpectation *grantedExpectation = [self
expectationWithDescription:@"Must complete without error if user choose to grant access"];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusNotDetermined);
self.mockService.cameraAuthorizationStatusStub = AVAuthorizationStatusNotDetermined;

// Mimic user choosing "allow" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(YES);
return YES;
}]]);
self.mockService.cameraGrantAccessStub = YES;

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
});
[self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

Expand All @@ -105,21 +134,16 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess {
message:@"User denied the camera access request."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusNotDetermined);
self.mockService.cameraAuthorizationStatusStub = AVAuthorizationStatusNotDetermined;

// Mimic user choosing "deny" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(NO);
return YES;
}]]);
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
self.mockService.cameraGrantAccessStub = NO;

[self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
}];

[self waitForExpectationsWithTimeout:1 handler:nil];
}
Expand All @@ -131,17 +155,16 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
[self expectationWithDescription:
@"Must copmlete without error if audio access was previously authorized."];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusAuthorized);
self.mockService.audioAuthorizationStatusStub = AVAuthorizationStatusAuthorized;

FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
[self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
if (error == nil) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied {
XCTestExpectation *expectation =
[self expectationWithDescription:
Expand All @@ -152,14 +175,13 @@ - (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied {
@"Settings to enable audio access."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusDenied);
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
self.mockService.audioAuthorizationStatusStub = AVAuthorizationStatusDenied;

[self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

Expand All @@ -170,37 +192,30 @@ - (void)testRequestAudioPermission_completeWithErrorIfRestricted {
message:@"Audio access is restricted. "
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusRestricted);
self.mockService.audioAuthorizationStatusStub = AVAuthorizationStatusRestricted;

FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
[self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess {
XCTestExpectation *grantedExpectation = [self
expectationWithDescription:@"Must complete without error if user choose to grant access"];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusNotDetermined);
self.mockService.audioAuthorizationStatusStub = AVAuthorizationStatusNotDetermined;

// Mimic user choosing "allow" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(YES);
return YES;
}]]);
self.mockService.audioGrantAccessStub = YES;

FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
[self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
});
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

Expand All @@ -211,22 +226,16 @@ - (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess {
message:@"User denied the audio access request."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusNotDetermined);
self.mockService.audioAuthorizationStatusStub = AVAuthorizationStatusNotDetermined;

// Mimic user choosing "deny" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(NO);
return YES;
}]]);
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
self.mockService.audioGrantAccessStub = NO;

[self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});

}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@import AVFoundation;
@import Flutter;

#import "./include/camera_avfoundation/CameraPermissionUtils.h"
#import "./include/camera_avfoundation/FLTCameraPermissionManager.h"
#import "./include/camera_avfoundation/CameraProperties.h"
#import "./include/camera_avfoundation/FLTCam.h"
#import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h"
Expand All @@ -25,6 +25,7 @@ @interface CameraPlugin ()
@property(readonly, nonatomic) id<FlutterTextureRegistry> registry;
@property(readonly, nonatomic) NSObject<FlutterBinaryMessenger> *messenger;
@property(nonatomic) FCPCameraGlobalEventApi *globalEventAPI;
@property(readonly, nonatomic) FLTCameraPermissionManager *permissionManager;
@end

@implementation CameraPlugin
Expand Down Expand Up @@ -52,6 +53,10 @@ - (instancetype)initWithRegistry:(NSObject<FlutterTextureRegistry> *)registry
_messenger = messenger;
_globalEventAPI = globalAPI;
_captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL);

id<FLTPermissionService> permissionService = [[FLTDefaultPermissionService alloc] init];
_permissionManager = [[FLTCameraPermissionManager alloc] initWithPermissionService:permissionService];

dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific,
(void *)FLTCaptureSessionQueueSpecific, NULL);

Expand Down Expand Up @@ -145,7 +150,7 @@ - (void)createCameraWithName:(nonnull NSString *)cameraName
// Create FLTCam only if granted camera access (and audio access if audio is enabled)
__weak typeof(self) weakSelf = self;
dispatch_async(self.captureSessionQueue, ^{
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
[self->_permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) {
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;

Expand All @@ -157,7 +162,7 @@ - (void)createCameraWithName:(nonnull NSString *)cameraName
// optional, and used as a workaround to fix a missing frame issue on iOS.
if (settings.enableAudio) {
// Setup audio capture session only if granted audio access.
FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
[self->_permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) {
// cannot use the outter `strongSelf`
typeof(self) strongSelf = weakSelf;
if (!strongSelf) return;
Expand All @@ -168,14 +173,14 @@ - (void)createCameraWithName:(nonnull NSString *)cameraName
settings:settings
completion:completion];
}
});
}];
} else {
[strongSelf createCameraOnSessionQueueWithName:cameraName
settings:settings
completion:completion];
}
}
});
}];
});
}

Expand Down
Loading