Files

622 lines
19 KiB
Dart

import "package:arcane_framework/arcane_framework.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:mocktail/mocktail.dart";
class MockArcaneAuthInterface extends Mock implements ArcaneAuthInterface {}
class MockAccountRegistration extends Mock
implements ArcaneAuthInterface, ArcaneAuthAccountRegistration {}
class MockPasswordManagement extends Mock
implements ArcaneAuthInterface, ArcaneAuthPasswordManagement {}
void main() {
group("ArcaneAuthenticationService error and edge cases", () {
setUp(() async {
await ArcaneAuthenticationService.I.reset();
Arcane.environment.reset();
});
test("reset clears interface and notifiers", () async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
await ArcaneAuthenticationService.I.reset();
expect(ArcaneAuthenticationService.I.authInterface, isNull);
expect(
ArcaneAuthenticationService.I.status,
AuthenticationStatus.unknown,
);
expect(ArcaneAuthenticationService.I.isSignedIn.value, false);
});
test("registerInterface throws if already registered", () async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
expect(
() async => ArcaneAuthenticationService.I.registerInterface(auth),
throwsException,
);
});
test("login returns error if no interface registered", () async {
final result = await ArcaneAuthenticationService.I.login(input: {});
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test("logOut returns error if no interface registered", () async {
final result = await ArcaneAuthenticationService.I.logOut();
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test("logOut returns error if not authenticated", () async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
final result = await ArcaneAuthenticationService.I.logOut();
expect(result.isFailure, true);
expect(result.error, contains("not authenticated"));
});
test("register returns error if no interface registered", () async {
final result = await ArcaneAuthenticationService.I.register(input: {});
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test("register returns error if interface does not support registration",
() async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
final result = await ArcaneAuthenticationService.I.register(input: {});
expect(result.isFailure, true);
expect(result.error, contains("does not support account registration"));
});
test("register returns error if registration returns null", () async {
final auth = MockAccountRegistration();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
when(() => auth.register(input: any(named: "input")))
.thenAnswer((_) async => const Result.error("returned a null value"));
await ArcaneAuthenticationService.I.registerInterface(auth);
final result = await ArcaneAuthenticationService.I.register(input: {});
expect(result.isFailure, true);
expect(result.error, contains("returned a null value"));
});
test("confirmSignup returns error if no interface registered", () async {
final result = await ArcaneAuthenticationService.I
.confirmSignup(email: "a", confirmationCode: "b");
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test(
"confirmSignup returns error if interface does not support registration",
() async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
final result = await ArcaneAuthenticationService.I
.confirmSignup(email: "a", confirmationCode: "b");
expect(result.isFailure, true);
expect(result.error, contains("does not support account registration"));
});
test("confirmSignup returns error if confirmSignup returns null", () async {
final auth = MockAccountRegistration();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
when(
() => auth.confirmSignup(
username: any(named: "username"),
confirmationCode: any(named: "confirmationCode"),
),
).thenAnswer((_) async => const Result.error("returned a null value"));
await ArcaneAuthenticationService.I.registerInterface(auth);
final result = await ArcaneAuthenticationService.I
.confirmSignup(email: "a", confirmationCode: "b");
expect(result.isFailure, true);
expect(result.error, contains("returned a null value"));
});
test("resendVerificationCode returns error if no interface registered",
() async {
final result =
await ArcaneAuthenticationService.I.resendVerificationCode("a");
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test(
"resendVerificationCode returns error if interface does not support registration",
() async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
final result =
await ArcaneAuthenticationService.I.resendVerificationCode("a");
expect(result.isFailure, true);
expect(result.error, contains("does not support account registration"));
});
test(
"resendVerificationCode returns error if resendVerificationCode returns null",
() async {
final auth = MockAccountRegistration();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
when(() => auth.resendVerificationCode(input: any(named: "input")))
.thenReturn(null);
await ArcaneAuthenticationService.I.registerInterface(auth);
final result =
await ArcaneAuthenticationService.I.resendVerificationCode("a");
expect(result.isFailure, true);
expect(result.error, contains("returned a null value"));
});
test("resetPassword returns error if no interface registered", () async {
final result =
await ArcaneAuthenticationService.I.resetPassword(email: "a");
expect(result.isFailure, true);
expect(result.error, contains("No ArcaneAuthInterface"));
});
test(
"resetPassword returns error if interface does not support password management",
() async {
final auth = MockArcaneAuthInterface();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
await ArcaneAuthenticationService.I.registerInterface(auth);
final result =
await ArcaneAuthenticationService.I.resetPassword(email: "a");
expect(result.isFailure, true);
expect(result.error, contains("does not support password management"));
});
test("resetPassword returns error if resetPassword returns null", () async {
final auth = MockPasswordManagement();
when(() => auth.init()).thenAnswer((_) async {
return null;
});
when(
() => auth.resetPassword(
email: any(named: "email"),
newPassword: any(named: "newPassword"),
code: any(named: "code"),
),
).thenAnswer((_) async => const Result.error("returned a null value"));
await ArcaneAuthenticationService.I.registerInterface(auth);
final result =
await ArcaneAuthenticationService.I.resetPassword(email: "a");
expect(result.isFailure, true);
expect(result.error, contains("returned a null value"));
});
test("dispose closes stream controllers and calls super", () async {
// Just call dispose to ensure no exceptions are thrown
ArcaneAuthenticationService.I.dispose();
// No assertion needed; just ensure no crash
});
test("notifier getter reflects unauthenticated default", () {
expect(
ArcaneAuthenticationService.I.notifier.value,
AuthenticationStatus.unknown,
);
});
test("isUnknown getter reflects unknown status", () {
expect(
ArcaneAuthenticationService.I.status.isUnknown,
isTrue,
);
});
test("isUnknown getter reflects unknown status after a reset", () async {
expect(
ArcaneAuthenticationService.I.status.isUnknown,
isTrue,
);
ArcaneAuthenticationService.I.setAuthenticated();
expect(
ArcaneAuthenticationService.I.status.isUnknown,
isFalse,
);
expect(
ArcaneAuthenticationService.I.status.isAuthenticated,
isTrue,
);
ArcaneAuthenticationService.I.setUnauthenticated();
expect(
ArcaneAuthenticationService.I.status.isUnknown,
isFalse,
);
expect(
ArcaneAuthenticationService.I.status.isAuthenticated,
isFalse,
);
expect(
ArcaneAuthenticationService.I.status.isUnauthenticated,
isTrue,
);
await ArcaneAuthenticationService.I.reset();
expect(
ArcaneAuthenticationService.I.status.isUnknown,
isTrue,
);
expect(
ArcaneAuthenticationService.I.status.isAuthenticated,
isFalse,
);
expect(
ArcaneAuthenticationService.I.status.isUnauthenticated,
isTrue,
);
});
test("accessToken and refreshToken fall back to empty string", () async {
expect(await ArcaneAuthenticationService.I.accessToken, "");
expect(await ArcaneAuthenticationService.I.refreshToken, "");
});
testWidgets("setDebug and setNormal callbacks run on state transitions",
(tester) async {
late BuildContext capturedContext;
var debugCallbackCalled = false;
var normalCallbackCalled = false;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
capturedContext = context;
return const SizedBox();
},
),
),
);
await ArcaneAuthenticationService.I.setDebug(
capturedContext,
onDebugModeSet: () async {
debugCallbackCalled = true;
},
);
await ArcaneAuthenticationService.I.setNormal(
capturedContext,
onDebugModeUnset: () async {
normalCallbackCalled = true;
},
);
expect(debugCallbackCalled, isTrue);
expect(normalCallbackCalled, isTrue);
});
});
late ArcaneAuthInterface authInterface;
group("ArcaneAuthenticationService", () {
setUp(() async {
authInterface = MockArcaneAuthInterface();
// Initialize the service
await ArcaneAuthenticationService.I.reset();
Arcane.environment.reset();
when(() => authInterface.init()).thenAnswer((_) async {});
when(
() => authInterface.login<Map<String, String>>(
input: any(named: "input"),
onLoggedIn: any(named: "onLoggedIn"),
),
).thenAnswer((_) async => const Result.ok(null));
when(
() => authInterface.logout(
onLoggedOut: any(named: "onLoggedOut"),
),
).thenAnswer((_) async => const Result.ok(null));
await ArcaneAuthenticationService.I.registerInterface(authInterface);
});
testWidgets("login with success", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: ArcaneEnvironmentProvider(
environment: Environment.normal,
child: Builder(
builder: (context) {
return Container();
},
),
),
),
);
final result = await ArcaneAuthenticationService.I.login(
input: {"username": "test"},
);
expect(result.isSuccess, true);
expect(
ArcaneAuthenticationService.I.status,
equals(AuthenticationStatus.authenticated),
);
});
testWidgets("login with failure", (WidgetTester tester) async {
when(
() => authInterface.login<Map<String, String>>(
input: any(named: "input"),
onLoggedIn: any(named: "onLoggedIn"),
),
).thenAnswer((_) async => const Result.error("error"));
final result = await ArcaneAuthenticationService.I
.login(input: {"username": "test"});
expect(result.isFailure, true);
expect(
ArcaneAuthenticationService.I.status,
equals(AuthenticationStatus.unknown),
);
});
testWidgets("logout with success", (WidgetTester tester) async {
ArcaneAuthenticationService.I.setAuthenticated();
final result = await ArcaneAuthenticationService.I.logOut();
expect(result.isSuccess, true);
expect(
ArcaneAuthenticationService.I.status,
equals(AuthenticationStatus.unauthenticated),
);
});
testWidgets("setDebug enables debug mode", (WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
home: ArcaneEnvironmentProvider(
child: Builder(
builder: (context) {
capturedContext = context;
return Container();
},
),
),
),
);
await tester.pump();
ArcaneEnvironment.of(capturedContext).enableDebugMode();
await tester.pump();
expect(
ArcaneEnvironment.of(capturedContext).environment,
equals(Environment.debug),
);
});
testWidgets(
"setDebug and setNormal use Arcane.environment without provider ancestry",
(WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
capturedContext = context;
return Container();
},
),
),
);
await ArcaneAuthenticationService.I.setDebug(capturedContext);
expect(Arcane.environment.current, Environment.debug);
await ArcaneAuthenticationService.I.setNormal(capturedContext);
expect(Arcane.environment.current, Environment.normal);
},
);
testWidgets("setNormal disables debug mode", (WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
home: ArcaneEnvironmentProvider(
child: Builder(
builder: (context) {
capturedContext = context;
return Container();
},
),
),
),
);
await tester.pump();
ArcaneEnvironment.of(capturedContext).enableDebugMode();
await tester.pump();
expect(
ArcaneEnvironment.of(capturedContext).environment,
equals(Environment.debug),
);
ArcaneEnvironment.of(capturedContext).disableDebugMode();
await tester.pump();
expect(
ArcaneEnvironment.of(capturedContext).environment,
equals(Environment.normal),
);
});
testWidgets(
"setDebug and setNormal do not mutate authentication status",
(WidgetTester tester) async {
late BuildContext capturedContext;
await tester.pumpWidget(
MaterialApp(
home: ArcaneEnvironmentProvider(
child: Builder(
builder: (context) {
capturedContext = context;
return Container();
},
),
),
),
);
await ArcaneAuthenticationService.I.login(
input: {"username": "test"},
);
expect(ArcaneAuthenticationService.I.isAuthenticated, true);
await ArcaneAuthenticationService.I.setDebug(capturedContext);
expect(
ArcaneAuthenticationService.I.status,
AuthenticationStatus.authenticated,
);
await ArcaneAuthenticationService.I.setNormal(capturedContext);
expect(
ArcaneAuthenticationService.I.status,
AuthenticationStatus.authenticated,
);
},
);
testWidgets("supports custom environment values",
(WidgetTester tester) async {
late BuildContext capturedContext;
const Environment staging = Environment("staging");
await tester.pumpWidget(
MaterialApp(
home: ArcaneEnvironmentProvider(
child: Builder(
builder: (context) {
capturedContext = context;
return Container();
},
),
),
),
);
ArcaneEnvironment.of(capturedContext).setEnvironment(staging);
await tester.pump();
expect(ArcaneEnvironment.of(capturedContext).environment, staging);
expect(ArcaneEnvironment.of(capturedContext).environment.name, "staging");
});
test("statusChanges emits authentication updates", () async {
final statusEvent = expectLater(
ArcaneAuthenticationService.I.statusChanges,
emits(AuthenticationStatus.authenticated),
);
ArcaneAuthenticationService.I.setAuthenticated();
await statusEvent;
});
test("signedInChanges emits signed-in updates", () async {
final signedInEvent = expectLater(
ArcaneAuthenticationService.I.signedInChanges,
emits(true),
);
ArcaneAuthenticationService.I.setAuthenticated();
await signedInEvent;
});
test("statusChanges works after listener cancellation", () async {
final firstSubscription =
ArcaneAuthenticationService.I.statusChanges.listen((_) {});
await firstSubscription.cancel();
// Ensure a deterministic baseline before asserting the next stream event.
ArcaneAuthenticationService.I.setUnauthenticated();
final secondEvent = expectLater(
ArcaneAuthenticationService.I.statusChanges,
emits(AuthenticationStatus.authenticated),
);
ArcaneAuthenticationService.I.setAuthenticated();
await secondEvent;
});
test("statusChanges and signedInChanges stay coherent", () async {
ArcaneAuthenticationService.I.setUnauthenticated();
final statusEvents = expectLater(
ArcaneAuthenticationService.I.statusChanges,
emitsInOrder(
<AuthenticationStatus>[
AuthenticationStatus.authenticated,
AuthenticationStatus.unauthenticated,
],
),
);
final signedInEvents = expectLater(
ArcaneAuthenticationService.I.signedInChanges,
emitsInOrder(<bool>[true, false]),
);
ArcaneAuthenticationService.I.setAuthenticated();
ArcaneAuthenticationService.I.setUnauthenticated();
await statusEvents;
await signedInEvents;
});
});
}