1
0
forked from baron/baron-sso

userfront&backend test coverage 추가

This commit is contained in:
2026-05-29 18:04:04 +09:00
parent 23cd316c23
commit 4c56c28481
26 changed files with 2405 additions and 260 deletions

View File

@@ -6,7 +6,18 @@ import 'auth_token_store.dart';
import 'log_policy.dart';
import 'runtime_env.dart';
typedef AuthProxyHttpClientFactory =
http.Client Function({bool withCredentials});
class AuthProxyService {
static AuthProxyHttpClientFactory? _httpClientFactoryForTesting;
static void debugSetHttpClientFactoryForTesting(
AuthProxyHttpClientFactory? factory,
) {
_httpClientFactoryForTesting = factory;
}
static String get _baseUrl => runtimeBackendUrl();
static bool get _isProd {
@@ -32,9 +43,71 @@ class AuthProxyService {
);
}
static http.Client _createClient({bool withCredentials = false}) {
final factory = _httpClientFactoryForTesting;
if (factory != null) {
return factory(withCredentials: withCredentials);
}
return createHttpClient(withCredentials: withCredentials);
}
static Future<http.Response> _get(
Uri url, {
Map<String, String>? headers,
bool withCredentials = false,
}) async {
final client = _createClient(withCredentials: withCredentials);
try {
return await client.get(url, headers: headers);
} finally {
client.close();
}
}
static Future<http.Response> _post(
Uri url, {
Map<String, String>? headers,
Object? body,
bool withCredentials = false,
}) async {
final client = _createClient(withCredentials: withCredentials);
try {
return await client.post(url, headers: headers, body: body);
} finally {
client.close();
}
}
static Future<http.Response> _patch(
Uri url, {
Map<String, String>? headers,
Object? body,
bool withCredentials = false,
}) async {
final client = _createClient(withCredentials: withCredentials);
try {
return await client.patch(url, headers: headers, body: body);
} finally {
client.close();
}
}
static Future<http.Response> _delete(
Uri url, {
Map<String, String>? headers,
bool withCredentials = false,
}) async {
final client = _createClient(withCredentials: withCredentials);
try {
return await client.delete(url, headers: headers);
} finally {
client.close();
}
}
static Future<Map<String, dynamic>> fetchPasswordPolicy() async {
final url = Uri.parse('$_baseUrl/api/v1/auth/password/policy');
final response = await http.get(url);
final response = await _get(url);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
@@ -47,24 +120,20 @@ class AuthProxyService {
static Future<Map<String, dynamic>> checkCookieSession() async {
final url = Uri.parse('$_baseUrl/api/v1/user/me');
final client = createHttpClient(withCredentials: true);
try {
final response = await client.get(
url,
headers: {'Content-Type': 'application/json'},
);
final response = await _get(
url,
headers: {'Content-Type': 'application/json'},
withCredentials: true,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
throw _error(
'err.userfront.auth_proxy.profile_load',
'Failed to load the profile: {{error}}',
detail: response.body,
);
} finally {
client.close();
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
throw _error(
'err.userfront.auth_proxy.profile_load',
'Failed to load the profile: {{error}}',
detail: response.body,
);
}
static Future<Map<String, dynamic>> getMe({
@@ -72,25 +141,24 @@ class AuthProxyService {
bool useCookie = true,
}) async {
final url = Uri.parse('$_baseUrl/api/v1/user/me');
final client = createHttpClient(withCredentials: useCookie);
try {
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.get(url, headers: headers);
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
throw _error(
'err.userfront.auth_proxy.profile_load',
'Failed to load the profile: {{error}}',
detail: response.body,
);
} finally {
client.close();
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await _get(
url,
headers: headers,
withCredentials: useCookie,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
throw _error(
'err.userfront.auth_proxy.profile_load',
'Failed to load the profile: {{error}}',
detail: response.body,
);
}
static Future<int> getSessionStatus({
@@ -98,22 +166,21 @@ class AuthProxyService {
bool useCookie = false,
}) async {
final url = Uri.parse('$_baseUrl/api/v1/user/me');
final client = createHttpClient(withCredentials: useCookie);
try {
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.get(url, headers: headers);
return response.statusCode;
} finally {
client.close();
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await _get(
url,
headers: headers,
withCredentials: useCookie,
);
return response.statusCode;
}
static Future<Map<String, dynamic>> getTenantInfo() async {
final url = Uri.parse('$_baseUrl/api/v1/auth/tenant-info');
final response = await http.get(url);
final response = await _get(url);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
@@ -144,7 +211,7 @@ class AuthProxyService {
body['codeOnly'] = true;
}
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
@@ -166,7 +233,7 @@ class AuthProxyService {
) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/enchanted-link/poll');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'pendingRef': pendingRef}),
@@ -191,7 +258,7 @@ class AuthProxyService {
}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/magic-link/verify');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'token': token, 'verifyOnly': verifyOnly}),
@@ -225,7 +292,7 @@ class AuthProxyService {
payload['pendingRef'] = pendingRef;
}
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
@@ -246,22 +313,21 @@ class AuthProxyService {
final url = Uri.parse('$_baseUrl/api/v1/user/sessions/$sessionId');
final useCookie = AuthTokenStore.usesCookie();
final token = AuthTokenStore.getToken();
final client = createHttpClient(withCredentials: useCookie);
try {
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.delete(url, headers: headers);
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.revoke',
'Failed to revoke the session: {{error}}',
detail: response.body,
);
}
} finally {
client.close();
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await _delete(
url,
headers: headers,
withCredentials: useCookie,
);
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.revoke',
'Failed to revoke the session: {{error}}',
detail: response.body,
);
}
}
@@ -269,35 +335,34 @@ class AuthProxyService {
final url = Uri.parse('$_baseUrl/api/v1/user/sessions');
final useCookie = AuthTokenStore.usesCookie();
final token = AuthTokenStore.getToken();
final client = createHttpClient(withCredentials: useCookie);
try {
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await client.get(url, headers: headers);
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.load',
'Failed to load the active sessions: {{error}}',
detail: response.body,
);
}
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final response = await _get(
url,
headers: headers,
withCredentials: useCookie,
);
if (response.statusCode != 200) {
throw _error(
'err.userfront.dashboard.sessions.load',
'Failed to load the active sessions: {{error}}',
detail: response.body,
);
}
final body = jsonDecode(response.body) as Map<String, dynamic>;
final items = (body['items'] as List?) ?? const [];
for (final item in items.whereType<Map<String, dynamic>>()) {
if (item['is_current'] == true) {
final sessionId = item['session_id']?.toString().trim() ?? '';
if (sessionId.isNotEmpty) {
return sessionId;
}
final body = jsonDecode(response.body) as Map<String, dynamic>;
final items = (body['items'] as List?) ?? const [];
for (final item in items.whereType<Map<String, dynamic>>()) {
if (item['is_current'] == true) {
final sessionId = item['session_id']?.toString().trim() ?? '';
if (sessionId.isNotEmpty) {
return sessionId;
}
}
return null;
} finally {
client.close();
}
return null;
}
static Future<Map<String, dynamic>> verifyLoginShortCode(
@@ -306,7 +371,7 @@ class AuthProxyService {
}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/login/code/verify-short');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'shortCode': shortCode, 'verifyOnly': verifyOnly}),
@@ -337,7 +402,7 @@ class AuthProxyService {
'login_challenge': loginChallenge,
};
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
@@ -360,29 +425,24 @@ class AuthProxyService {
final url = Uri.parse(
'$_baseUrl/api/v1/auth/consent',
).replace(queryParameters: {'consent_challenge': consentChallenge});
final client = createHttpClient(withCredentials: true);
try {
final response = await client.get(
url,
headers: {'Content-Type': 'application/json'},
);
final response = await _get(
url,
headers: {'Content-Type': 'application/json'},
withCredentials: true,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
final rawDetails = errorBody['details'];
throw AuthProxyException(
errorCode: (errorBody['code'] ?? '').toString(),
message:
(errorBody['error'] ??
tr('err.userfront.auth_proxy.consent_fetch'))
.toString(),
details: rawDetails is Map<String, dynamic> ? rawDetails : null,
);
}
} finally {
client.close();
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
final rawDetails = errorBody['details'];
throw AuthProxyException(
errorCode: (errorBody['code'] ?? '').toString(),
message:
(errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_fetch'))
.toString(),
details: rawDetails is Map<String, dynamic> ? rawDetails : null,
);
}
}
@@ -396,24 +456,20 @@ class AuthProxyService {
body['grant_scope'] = grantScope;
}
final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
);
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
withCredentials: true,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'),
);
}
} finally {
client.close();
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'),
);
}
}
@@ -423,24 +479,20 @@ class AuthProxyService {
final url = Uri.parse('$_baseUrl/api/v1/auth/consent/reject');
final body = <String, dynamic>{'consent_challenge': consentChallenge};
final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
);
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(body),
withCredentials: true,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'),
);
}
} finally {
client.close();
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'),
);
}
}
@@ -453,24 +505,20 @@ class AuthProxyService {
if (token != null && token.isNotEmpty) {
headers['Authorization'] = 'Bearer $token';
}
final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url,
headers: headers,
body: jsonEncode({'login_challenge': loginChallenge}),
);
final response = await _post(
url,
headers: headers,
body: jsonEncode({'login_challenge': loginChallenge}),
withCredentials: true,
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept'),
);
}
} finally {
client.close();
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept'),
);
}
}
@@ -479,7 +527,7 @@ class AuthProxyService {
bool? drySend,
}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
@@ -514,7 +562,7 @@ class AuthProxyService {
final url = Uri.parse(
'$_baseUrl/api/v1/auth/password/reset/complete',
).replace(queryParameters: query);
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'newPassword': newPassword}),
@@ -534,7 +582,7 @@ class AuthProxyService {
static Future<void> sendSms(String phoneNumber) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/sms');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phoneNumber': phoneNumber}),
@@ -555,7 +603,7 @@ class AuthProxyService {
) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/verify-sms');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phoneNumber': phoneNumber, 'code': code}),
@@ -574,7 +622,7 @@ class AuthProxyService {
static Future<Map<String, dynamic>> initQrLogin() async {
final url = Uri.parse('$_baseUrl/api/v1/auth/qr/init');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
);
@@ -592,7 +640,7 @@ class AuthProxyService {
static Future<Map<String, dynamic>> pollQrStatus(String pendingRef) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/qr/poll');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'pendingRef': pendingRef}),
@@ -624,39 +672,26 @@ class AuthProxyService {
payload['token'] = token;
}
http.Client? client;
try {
if (withCredentials) {
client = createHttpClient(withCredentials: true);
}
final response = await (client != null
? client.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
)
: http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
));
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
withCredentials: withCredentials,
);
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.qr_approve',
'Failed to approve QR login: {{error}}',
detail: response.body,
);
}
} finally {
client?.close();
if (response.statusCode != 200) {
throw _error(
'err.userfront.auth_proxy.qr_approve',
'Failed to approve QR login: {{error}}',
detail: response.body,
);
}
}
static Future<bool> checkAdminAuth(String adminPassword) async {
final url = Uri.parse('$_baseUrl/api/v1/admin/check');
try {
final response = await http.get(
final response = await _get(
url,
headers: {
'Content-Type': 'application/json',
@@ -678,7 +713,7 @@ class AuthProxyService {
}) async {
final url = Uri.parse('$_baseUrl/api/v1/admin/users');
final response = await http.post(
final response = await _post(
url,
headers: {
'Content-Type': 'application/json',
@@ -710,7 +745,7 @@ class AuthProxyService {
uri = uri.replace(queryParameters: {'text': query});
}
final response = await http.get(
final response = await _get(
uri,
headers: {
'Content-Type': 'application/json',
@@ -734,7 +769,7 @@ class AuthProxyService {
final encodedId = Uri.encodeComponent(loginId);
final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId');
final response = await http.delete(
final response = await _delete(
url,
headers: {
'Content-Type': 'application/json',
@@ -759,7 +794,7 @@ class AuthProxyService {
final encodedId = Uri.encodeComponent(loginId);
final url = Uri.parse('$_baseUrl/api/v1/admin/users/$encodedId/status');
final response = await http.patch(
final response = await _patch(
url,
headers: {
'Content-Type': 'application/json',
@@ -792,7 +827,7 @@ class AuthProxyService {
if (phone != null) body['phone'] = phone;
if (displayName != null) body['displayName'] = displayName;
final response = await http.patch(
final response = await _patch(
url,
headers: {
'Content-Type': 'application/json',
@@ -815,26 +850,25 @@ class AuthProxyService {
final useCookie = AuthTokenStore.usesCookie();
final token = AuthTokenStore.getToken();
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
try {
final response = await client.get(url, headers: headers);
final response = await _get(
url,
headers: headers,
withCredentials: useCookie,
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['items'] ?? [];
} else {
throw _error(
'err.userfront.auth_proxy.linked_apps_load',
'Failed to load linked applications.',
);
}
} finally {
client.close();
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['items'] ?? [];
} else {
throw _error(
'err.userfront.auth_proxy.linked_apps_load',
'Failed to load linked applications.',
);
}
}
@@ -843,24 +877,22 @@ class AuthProxyService {
final useCookie = AuthTokenStore.usesCookie();
final token = AuthTokenStore.getToken();
final client = createHttpClient(withCredentials: useCookie);
final headers = <String, String>{'Content-Type': 'application/json'};
if (!useCookie && token != null) {
headers['Authorization'] = 'Bearer $token';
}
try {
final response = await client.delete(url, headers: headers);
final response = await _delete(
url,
headers: headers,
withCredentials: useCookie,
);
if (response.statusCode != 200) {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ??
tr('err.userfront.auth_proxy.linked_app_revoke'),
);
}
} finally {
client.close();
if (response.statusCode != 200) {
final errorBody = jsonDecode(response.body);
throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.linked_app_revoke'),
);
}
}
@@ -888,7 +920,7 @@ class AuthProxyService {
final sanitizedMessage = LogPolicy.sanitizeMessage(message);
final sanitizedData = data == null ? null : LogPolicy.sanitizeData(data);
try {
await http.post(
await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
@@ -945,7 +977,7 @@ class AuthProxyService {
static Future<bool> checkEmailAvailability(String email) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/signup/check-email');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': email}),
@@ -967,7 +999,7 @@ class AuthProxyService {
if (tenantSlug != null && tenantSlug.isNotEmpty) {
bodyData['tenantSlug'] = tenantSlug;
}
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(bodyData),
@@ -997,7 +1029,7 @@ class AuthProxyService {
}
final url = Uri.parse(uriString);
final response = await http.get(url);
final response = await _get(url);
if (response.statusCode == 200) {
final List<dynamic> list = jsonDecode(response.body);
return list.cast<Map<String, dynamic>>();
@@ -1009,7 +1041,7 @@ class AuthProxyService {
final path = type == 'email' ? 'send-email-code' : 'send-sms-code';
final url = Uri.parse('$_baseUrl/api/v1/auth/signup/$path');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'target': target}),
@@ -1031,7 +1063,7 @@ class AuthProxyService {
) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/signup/verify-code');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'target': target, 'type': type, 'code': code}),
@@ -1055,7 +1087,7 @@ class AuthProxyService {
}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/signup');
final response = await http.post(
final response = await _post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({