Stabilize auth flow and profile images
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:tdc114plus/src/core/network/api_error.dart';
|
||||
import 'package:tdc114plus/src/features/auth/data/auth_api_client.dart';
|
||||
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
|
||||
|
||||
void main() {
|
||||
test('posts phone login request to tdc114plus namespace', () async {
|
||||
final client = AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(
|
||||
request.url.toString(),
|
||||
'https://sso.example.test/api/v1/tdc114plus/auth/phone-login',
|
||||
);
|
||||
expect(request.method, 'POST');
|
||||
expect(request.headers['content-type'], 'application/json');
|
||||
expect(jsonDecode(request.body)['phoneNumber'], '01012345678');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'ok',
|
||||
'token': 'session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
},
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
);
|
||||
|
||||
final response = await client.phoneLogin(
|
||||
const PhoneLoginRequest(
|
||||
phoneNumber: '01012345678',
|
||||
device: LoginDeviceInfo(
|
||||
platform: 'android',
|
||||
appVersion: '0.1.0',
|
||||
deviceName: 'Pixel 8',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.token, 'session-token');
|
||||
expect(response.user.tenantSlug, 'hanmac');
|
||||
});
|
||||
|
||||
test('throws ApiException when login fails', () async {
|
||||
final client = AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'error': 'login failed',
|
||||
'code': 'login_failed',
|
||||
'details': {},
|
||||
}),
|
||||
401,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => client.phoneLogin(
|
||||
const PhoneLoginRequest(
|
||||
phoneNumber: '01012345678',
|
||||
device: LoginDeviceInfo(
|
||||
platform: 'android',
|
||||
appVersion: '0.1.0',
|
||||
deviceName: 'Pixel 8',
|
||||
),
|
||||
),
|
||||
),
|
||||
throwsA(
|
||||
isA<ApiException>()
|
||||
.having((error) => error.statusCode, 'statusCode', 401)
|
||||
.having((error) => error.code, 'code', 'login_failed'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('requests phone link from auth server namespace', () async {
|
||||
final client = AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(
|
||||
request.url.toString(),
|
||||
'https://sso.example.test/api/v1/auth/link/init',
|
||||
);
|
||||
expect(request.method, 'POST');
|
||||
expect(jsonDecode(request.body)['phoneNumber'], '01012345678');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'pending',
|
||||
'pendingRef': 'pending-ref',
|
||||
'expiresIn': 180,
|
||||
'pollInterval': 3,
|
||||
'resendAfter': 30,
|
||||
'provider': 'Ory (Kratos/Hydra)',
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
);
|
||||
|
||||
final response = await client.requestPhoneLoginLink(
|
||||
const PhoneLoginLinkInitRequest(
|
||||
phoneNumber: '01012345678',
|
||||
device: LoginDeviceInfo(
|
||||
platform: 'android',
|
||||
appVersion: '0.1.0',
|
||||
deviceName: 'Pixel 8',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.pendingRef, 'pending-ref');
|
||||
expect(response.interval, 3);
|
||||
});
|
||||
|
||||
test('polls headless phone link and parses completed session', () async {
|
||||
final client = AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(
|
||||
request.url.toString(),
|
||||
'https://sso.example.test/api/v1/auth/link/poll',
|
||||
);
|
||||
expect(jsonDecode(request.body)['pendingRef'], 'pending-ref');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'ok',
|
||||
'session': {
|
||||
'status': 'ok',
|
||||
'accessToken': 'session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
},
|
||||
},
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
);
|
||||
|
||||
final response = await client.pollPhoneLoginLink(
|
||||
const PhoneLoginLinkPollRequest(pendingRef: 'pending-ref'),
|
||||
);
|
||||
|
||||
expect(response.session?.token, 'session-token');
|
||||
expect(response.session?.user.tenantSlug, 'hanmac');
|
||||
});
|
||||
|
||||
test('parses completed auth-server poll response with top-level accessToken', () async {
|
||||
final client = AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(
|
||||
request.url.toString(),
|
||||
'https://auth.example.test/api/v1/auth/link/poll',
|
||||
);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'ok',
|
||||
'accessToken': 'app-session-jwt',
|
||||
'expiresAt': '2026-07-13T16:32:56+09:00',
|
||||
'user': {
|
||||
'id': 'baron-user',
|
||||
'name': 'Baron User',
|
||||
'phoneNumber': '01091365338',
|
||||
},
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://auth.example.test'),
|
||||
);
|
||||
|
||||
final response = await client.pollPhoneLoginLink(
|
||||
const PhoneLoginLinkPollRequest(pendingRef: 'pending-ref'),
|
||||
);
|
||||
|
||||
expect(response.status, 'ok');
|
||||
expect(response.session?.token, 'app-session-jwt');
|
||||
expect(response.session?.user.phoneNumber, '01091365338');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tdc114plus/src/core/network/api_error.dart';
|
||||
import 'package:tdc114plus/src/features/auth/data/auth_api_client.dart';
|
||||
import 'package:tdc114plus/src/features/auth/data/auth_repository.dart';
|
||||
import 'package:tdc114plus/src/features/auth/data/auth_session_store.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('normalizes phone number and stores successful session', () async {
|
||||
final repository = RemoteAuthRepository(
|
||||
apiClient: AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(jsonDecode(request.body)['phoneNumber'], '01012345678');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'ok',
|
||||
'token': 'session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
|
||||
await repository.phoneLogin('010-1234-5678');
|
||||
final session = await repository.loadSession();
|
||||
|
||||
expect(session?.token, 'session-token');
|
||||
expect(session?.user.id, 'user-uuid');
|
||||
});
|
||||
|
||||
test('rejects invalid phone number before calling API', () async {
|
||||
var called = false;
|
||||
final repository = RemoteAuthRepository(
|
||||
apiClient: AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
called = true;
|
||||
return http.Response('{}', 200);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
|
||||
expect(
|
||||
() => repository.phoneLogin('123'),
|
||||
throwsA(
|
||||
isA<ApiException>().having(
|
||||
(error) => error.code,
|
||||
'code',
|
||||
'invalid_phone_number',
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(called, isFalse);
|
||||
});
|
||||
|
||||
test('normalizes phone number before requesting login link', () async {
|
||||
final repository = RemoteAuthRepository(
|
||||
apiClient: AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/auth/link/init');
|
||||
expect(jsonDecode(request.body)['phoneNumber'], '01012345678');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'pending',
|
||||
'pendingRef': 'pending-ref',
|
||||
'expiresIn': 180,
|
||||
'pollInterval': 3,
|
||||
'resendAfter': 30,
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
|
||||
final response = await repository.requestPhoneLoginLink('010-1234-5678');
|
||||
|
||||
expect(response.pendingRef, 'pending-ref');
|
||||
});
|
||||
|
||||
test('stores session when login link polling completes', () async {
|
||||
final repository = RemoteAuthRepository(
|
||||
apiClient: AuthApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.path, '/api/v1/auth/link/poll');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'status': 'ok',
|
||||
'accessToken': 'session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
appVersion: '0.1.0',
|
||||
);
|
||||
|
||||
final response = await repository.pollPhoneLoginLink('pending-ref');
|
||||
final session = await repository.loadSession();
|
||||
|
||||
expect(response.session?.token, 'session-token');
|
||||
expect(session?.token, 'session-token');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tdc114plus/src/features/auth/data/auth_session_store.dart';
|
||||
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('saves loads and clears login session', () async {
|
||||
const store = AuthSessionStore();
|
||||
final response = PhoneLoginResponse(
|
||||
status: 'ok',
|
||||
token: 'session-token',
|
||||
expiresAt: DateTime.parse('2026-07-02T12:00:00Z'),
|
||||
user: const LoginUser(
|
||||
id: 'user-uuid',
|
||||
name: 'User One',
|
||||
phoneNumber: '+821012345678',
|
||||
tenantId: 'tenant-uuid',
|
||||
tenantName: 'Hanmac',
|
||||
tenantSlug: 'hanmac',
|
||||
),
|
||||
orgContextCredential: OrgContextCredential(
|
||||
baseUrl: 'https://sadmin.hmac.kr',
|
||||
tenantSlug: 'hanmac-family',
|
||||
keyId: 'session-key-id',
|
||||
keySecret: 'session-key-secret',
|
||||
expiresAt: DateTime.parse('2099-07-02T13:00:00Z'),
|
||||
),
|
||||
);
|
||||
|
||||
await store.save(response);
|
||||
final loaded = await store.load();
|
||||
|
||||
expect(loaded?.token, 'session-token');
|
||||
expect(loaded?.user.name, 'User One');
|
||||
expect(loaded?.orgContextCredential?.keyId, 'session-key-id');
|
||||
expect(loaded?.orgContextCredential?.keySecret, 'session-key-secret');
|
||||
|
||||
await store.clear();
|
||||
expect(await store.load(), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user