353 lines
12 KiB
Dart
353 lines
12 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:auth/auth.dart';
|
|
|
|
void main() {
|
|
// ---------------------------------------------------------------------------
|
|
// KcUser
|
|
// ---------------------------------------------------------------------------
|
|
group('KcUser', () {
|
|
const user = KcUser(
|
|
id: 'u1',
|
|
displayName: 'Alice',
|
|
email: 'alice@example.com',
|
|
role: KcUserRole.operator,
|
|
);
|
|
|
|
test('stores all fields', () {
|
|
expect(user.id, 'u1');
|
|
expect(user.displayName, 'Alice');
|
|
expect(user.email, 'alice@example.com');
|
|
expect(user.role, KcUserRole.operator);
|
|
});
|
|
|
|
test('copyWith replaces specified fields', () {
|
|
final updated = user.copyWith(displayName: 'Bob', role: KcUserRole.admin);
|
|
expect(updated.id, 'u1');
|
|
expect(updated.displayName, 'Bob');
|
|
expect(updated.email, 'alice@example.com');
|
|
expect(updated.role, KcUserRole.admin);
|
|
});
|
|
|
|
test('copyWith with no args returns equivalent user', () {
|
|
final copy = user.copyWith();
|
|
expect(copy, equals(user));
|
|
});
|
|
|
|
test('equality compares all fields', () {
|
|
const same = KcUser(
|
|
id: 'u1',
|
|
displayName: 'Alice',
|
|
email: 'alice@example.com',
|
|
role: KcUserRole.operator,
|
|
);
|
|
const different = KcUser(
|
|
id: 'u2',
|
|
displayName: 'Alice',
|
|
email: 'alice@example.com',
|
|
role: KcUserRole.operator,
|
|
);
|
|
expect(user, equals(same));
|
|
expect(user, isNot(equals(different)));
|
|
expect(user.hashCode, equals(same.hashCode));
|
|
});
|
|
|
|
test('toString includes key fields', () {
|
|
expect(user.toString(), contains('u1'));
|
|
expect(user.toString(), contains('Alice'));
|
|
expect(user.toString(), contains('operator'));
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// KcUserRole
|
|
// ---------------------------------------------------------------------------
|
|
group('KcUserRole', () {
|
|
test('has three values', () {
|
|
expect(KcUserRole.values.length, 3);
|
|
});
|
|
|
|
test('values are admin, operator, viewer', () {
|
|
expect(
|
|
KcUserRole.values,
|
|
containsAll([KcUserRole.admin, KcUserRole.operator, KcUserRole.viewer]),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AuthState
|
|
// ---------------------------------------------------------------------------
|
|
group('AuthState', () {
|
|
const user = KcUser(
|
|
id: 'u1',
|
|
displayName: 'Alice',
|
|
email: 'alice@example.com',
|
|
role: KcUserRole.admin,
|
|
);
|
|
|
|
test('unauthenticated: isAuthenticated is false', () {
|
|
const state = AuthStateUnauthenticated();
|
|
expect(state.isAuthenticated, isFalse);
|
|
expect(state.isLoading, isFalse);
|
|
expect(state.user, isNull);
|
|
});
|
|
|
|
test('loading: isLoading is true', () {
|
|
const state = AuthStateLoading();
|
|
expect(state.isLoading, isTrue);
|
|
expect(state.isAuthenticated, isFalse);
|
|
expect(state.user, isNull);
|
|
});
|
|
|
|
test('authenticated: isAuthenticated is true and user is set', () {
|
|
final state = AuthStateAuthenticated(user: user);
|
|
expect(state.isAuthenticated, isTrue);
|
|
expect(state.isLoading, isFalse);
|
|
expect(state.user, equals(user));
|
|
});
|
|
|
|
test('authenticated: equality compares user', () {
|
|
final a = AuthStateAuthenticated(user: user);
|
|
final b = AuthStateAuthenticated(user: user);
|
|
const c = KcUser(
|
|
id: 'u2',
|
|
displayName: 'Bob',
|
|
email: 'bob@example.com',
|
|
role: KcUserRole.viewer,
|
|
);
|
|
final d = AuthStateAuthenticated(user: c);
|
|
expect(a, equals(b));
|
|
expect(a, isNot(equals(d)));
|
|
expect(a.hashCode, equals(b.hashCode));
|
|
});
|
|
|
|
test('error: message is stored', () {
|
|
const state = AuthStateError(message: 'Invalid credentials');
|
|
expect(state.isAuthenticated, isFalse);
|
|
expect(state.user, isNull);
|
|
expect(state.message, 'Invalid credentials');
|
|
});
|
|
|
|
test('error: equality compares message', () {
|
|
const a = AuthStateError(message: 'oops');
|
|
const b = AuthStateError(message: 'oops');
|
|
const c = AuthStateError(message: 'different');
|
|
expect(a, equals(b));
|
|
expect(a, isNot(equals(c)));
|
|
});
|
|
|
|
test('toString includes state type', () {
|
|
expect(const AuthStateUnauthenticated().toString(), contains('unauthenticated'));
|
|
expect(const AuthStateLoading().toString(), contains('loading'));
|
|
expect(AuthStateAuthenticated(user: user).toString(), contains('authenticated'));
|
|
expect(const AuthStateError(message: 'x').toString(), contains('error'));
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AuthException
|
|
// ---------------------------------------------------------------------------
|
|
group('AuthException', () {
|
|
test('stores message and optional statusCode', () {
|
|
const ex = AuthException(message: 'Unauthorized', statusCode: 401);
|
|
expect(ex.message, 'Unauthorized');
|
|
expect(ex.statusCode, 401);
|
|
});
|
|
|
|
test('toString includes message', () {
|
|
const ex = AuthException(message: 'Forbidden');
|
|
expect(ex.toString(), contains('Forbidden'));
|
|
});
|
|
|
|
test('toString includes HTTP status when present', () {
|
|
const ex = AuthException(message: 'Unauthorized', statusCode: 401);
|
|
expect(ex.toString(), contains('401'));
|
|
});
|
|
|
|
test('statusCode is optional', () {
|
|
const ex = AuthException(message: 'Network error');
|
|
expect(ex.statusCode, isNull);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CredentialStore / CredentialKeys
|
|
// ---------------------------------------------------------------------------
|
|
group('InMemoryCredentialStore', () {
|
|
late InMemoryCredentialStore store;
|
|
|
|
setUp(() => store = InMemoryCredentialStore());
|
|
|
|
test('read returns null for missing key', () async {
|
|
expect(await store.read('missing'), isNull);
|
|
});
|
|
|
|
test('write and read round-trip', () async {
|
|
await store.write(CredentialKeys.wcConsumerKey, 'ck_test');
|
|
expect(await store.read(CredentialKeys.wcConsumerKey), 'ck_test');
|
|
});
|
|
|
|
test('write overwrites existing value', () async {
|
|
await store.write('key', 'first');
|
|
await store.write('key', 'second');
|
|
expect(await store.read('key'), 'second');
|
|
});
|
|
|
|
test('delete removes key', () async {
|
|
await store.write('key', 'value');
|
|
await store.delete('key');
|
|
expect(await store.read('key'), isNull);
|
|
});
|
|
|
|
test('delete is no-op for missing key', () async {
|
|
await expectLater(store.delete('missing'), completes);
|
|
});
|
|
|
|
test('deleteAll clears all keys', () async {
|
|
await store.write('a', '1');
|
|
await store.write('b', '2');
|
|
await store.deleteAll();
|
|
expect(await store.read('a'), isNull);
|
|
expect(await store.read('b'), isNull);
|
|
});
|
|
|
|
test('containsKey returns true for present key', () async {
|
|
await store.write('key', 'value');
|
|
expect(await store.containsKey('key'), isTrue);
|
|
});
|
|
|
|
test('containsKey returns false for missing key', () async {
|
|
expect(await store.containsKey('missing'), isFalse);
|
|
});
|
|
});
|
|
|
|
group('CredentialKeys', () {
|
|
test('all keys are non-empty strings', () {
|
|
expect(CredentialKeys.wcConsumerKey, isNotEmpty);
|
|
expect(CredentialKeys.wcConsumerSecret, isNotEmpty);
|
|
expect(CredentialKeys.wcSiteUrl, isNotEmpty);
|
|
expect(CredentialKeys.squareAccessToken, isNotEmpty);
|
|
expect(CredentialKeys.squareLocationId, isNotEmpty);
|
|
expect(CredentialKeys.sessionToken, isNotEmpty);
|
|
});
|
|
|
|
test('all keys are unique', () {
|
|
final keys = [
|
|
CredentialKeys.wcConsumerKey,
|
|
CredentialKeys.wcConsumerSecret,
|
|
CredentialKeys.wcSiteUrl,
|
|
CredentialKeys.squareAccessToken,
|
|
CredentialKeys.squareLocationId,
|
|
CredentialKeys.sessionToken,
|
|
];
|
|
expect(keys.toSet().length, keys.length);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FakeAuthService
|
|
// ---------------------------------------------------------------------------
|
|
group('FakeAuthService', () {
|
|
late FakeAuthService service;
|
|
|
|
setUp(() => service = FakeAuthService());
|
|
tearDown(() => service.dispose());
|
|
|
|
test('initial state is unauthenticated', () {
|
|
expect(service.currentState, isA<AuthStateUnauthenticated>());
|
|
expect(service.currentUser, isNull);
|
|
});
|
|
|
|
test('authStateStream emits current state immediately', () async {
|
|
final first = await service.authStateStream.first;
|
|
expect(first, isA<AuthStateUnauthenticated>());
|
|
});
|
|
|
|
test('signIn with valid credentials returns user', () async {
|
|
final user = await service.signIn(email: 'operator@kellcreations.com', password: 'secret');
|
|
expect(user.email, 'operator@kellcreations.com');
|
|
expect(user.role, KcUserRole.operator);
|
|
});
|
|
|
|
test('signIn sets state to authenticated', () async {
|
|
await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
expect(service.currentState, isA<AuthStateAuthenticated>());
|
|
expect(service.currentUser?.role, KcUserRole.admin);
|
|
});
|
|
|
|
test('signIn with admin email assigns admin role', () async {
|
|
final user = await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
expect(user.role, KcUserRole.admin);
|
|
});
|
|
|
|
test('signIn with operator email assigns operator role', () async {
|
|
final user = await service.signIn(email: 'operator@kellcreations.com', password: 'pw');
|
|
expect(user.role, KcUserRole.operator);
|
|
});
|
|
|
|
test('signIn with unknown email assigns viewer role', () async {
|
|
final user = await service.signIn(email: 'someone@example.com', password: 'pw');
|
|
expect(user.role, KcUserRole.viewer);
|
|
});
|
|
|
|
test('signIn with empty email throws AuthException', () async {
|
|
await expectLater(service.signIn(email: '', password: 'pw'), throwsA(isA<AuthException>()));
|
|
expect(service.currentState, isA<AuthStateError>());
|
|
});
|
|
|
|
test('signIn with empty password throws AuthException', () async {
|
|
await expectLater(
|
|
service.signIn(email: 'user@example.com', password: ''),
|
|
throwsA(isA<AuthException>()),
|
|
);
|
|
expect(service.currentState, isA<AuthStateError>());
|
|
});
|
|
|
|
test('signOut sets state to unauthenticated', () async {
|
|
await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
await service.signOut();
|
|
expect(service.currentState, isA<AuthStateUnauthenticated>());
|
|
expect(service.currentUser, isNull);
|
|
});
|
|
|
|
test('refreshSession returns true when authenticated', () async {
|
|
await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
expect(await service.refreshSession(), isTrue);
|
|
});
|
|
|
|
test('refreshSession returns false when unauthenticated', () async {
|
|
expect(await service.refreshSession(), isFalse);
|
|
});
|
|
|
|
test('authStateStream emits loading then authenticated on signIn', () async {
|
|
final states = <AuthState>[];
|
|
final sub = service.authStateStream.listen(states.add);
|
|
|
|
await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
// Allow any pending microtasks to flush.
|
|
await Future<void>.delayed(Duration.zero);
|
|
await sub.cancel();
|
|
|
|
// Stream: unauthenticated (initial), loading, authenticated
|
|
expect(states.length, greaterThanOrEqualTo(2));
|
|
expect(states.any((s) => s is AuthStateLoading), isTrue);
|
|
expect(states.last, isA<AuthStateAuthenticated>());
|
|
});
|
|
|
|
test('authStateStream emits loading then unauthenticated on signOut', () async {
|
|
await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
|
|
|
|
final states = <AuthState>[];
|
|
final sub = service.authStateStream.listen(states.add);
|
|
|
|
await service.signOut();
|
|
// Allow any pending microtasks to flush.
|
|
await Future<void>.delayed(Duration.zero);
|
|
await sub.cancel();
|
|
|
|
expect(states.last, isA<AuthStateUnauthenticated>());
|
|
});
|
|
});
|
|
}
|