Stabilize auth flow and profile images

This commit is contained in:
Codex
2026-07-20 13:38:39 +09:00
parent 57caca8dc8
commit 5d3eee7a16
128 changed files with 28860 additions and 1468 deletions
@@ -0,0 +1,371 @@
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/features/directory/data/directory_repository.dart';
import 'package:tdc114plus/src/features/auth/data/auth_session_store.dart';
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
import 'package:tdc114plus/src/features/organization/data/org_context_api_client.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
OrgContextApiClient.clearCacheForTesting();
});
test('maps Baron org-context response to app org chart snapshot', () async {
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.path, '/api/v1/integrations/org-context');
expect(request.url.queryParameters['tenantSlug'], 'hanmac-family');
expect(request.url.queryParameters['includeUsers'], 'true');
expect(request.url.queryParameters['includeUserIds'], 'true');
expect(request.headers['X-Baron-Key-ID'], 'key-id');
expect(request.headers['X-Baron-Key-Secret'], 'key-secret');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family',
);
final snapshot = await client.fetchOrgContext();
expect(snapshot.tenants.map((tenant) => tenant.slug), contains('is3'));
expect(snapshot.employees, hasLength(4));
expect(snapshot.employees.first.tenantName, 'IS3');
expect(snapshot.employees.first.phoneDisplay, '010-3189-1514');
final center = snapshot.tenants.singleWhere(
(tenant) => tenant.slug == 'center',
);
final is3 = snapshot.tenants.singleWhere((tenant) => tenant.slug == 'is3');
expect(center.totalMemberCount, 4);
expect(is3.memberCount, 2);
expect(is3.totalMemberCount, 2);
});
test('uses session org-context credential before env fallback', () async {
const store = AuthSessionStore();
await store.save(
PhoneLoginResponse(
status: 'ok',
token: 'session-token',
expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
user: const LoginUser(
id: 'user-id',
name: 'User',
phoneNumber: '',
tenantId: '',
tenantName: '',
tenantSlug: '',
),
orgContextCredential: const OrgContextCredential(
baseUrl: 'https://session.example.test',
tenantSlug: 'session-family',
keyId: 'session-key-id',
keySecret: 'session-key-secret',
),
),
);
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.host, 'session.example.test');
expect(request.url.queryParameters['tenantSlug'], 'session-family');
expect(request.headers['Authorization'], 'Bearer session-token');
expect(request.headers['X-Baron-Key-ID'], 'session-key-id');
expect(request.headers['X-Baron-Key-Secret'], 'session-key-secret');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://env.example.test'),
keyId: 'env-key-id',
keySecret: 'env-key-secret',
tenantSlug: 'env-family',
sessionStore: store,
);
await client.fetchOrgContext();
});
test('uses requested tenant slug for scoped org-context reads', () async {
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.queryParameters['tenantSlug'], 'is3');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family',
);
await client.fetchOrgContext(tenantSlug: 'is3');
});
test('caches org-context response by requested tenant slug', () async {
final requestedTenantSlugs = <String>[];
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
requestedTenantSlugs.add(request.url.queryParameters['tenantSlug']!);
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family',
);
await client.fetchOrgContext(tenantSlug: 'is3');
await client.fetchOrgContext(tenantSlug: 'is3');
await client.fetchOrgContext(tenantSlug: 'samahn');
expect(requestedTenantSlugs, ['is3', 'samahn']);
});
test('uses app session bearer token for auth-server org-context proxy', () async {
const store = AuthSessionStore();
await store.save(
PhoneLoginResponse(
status: 'ok',
token: 'app-session-token',
expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
user: const LoginUser(
id: 'user-id',
name: 'User',
phoneNumber: '',
tenantId: '',
tenantName: '',
tenantSlug: '',
),
),
);
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.host, '114-auth.hmac.kr');
expect(request.url.path, '/api/v1/integrations/org-context');
expect(request.headers['Authorization'], 'Bearer app-session-token');
expect(request.headers.containsKey('X-Baron-Key-ID'), isFalse);
expect(request.headers.containsKey('X-Baron-Key-Secret'), isFalse);
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family',
sessionStore: store,
);
await client.fetchOrgContext();
});
test('caches org-context response for repeated badge navigation reads', () async {
var requestCount = 0;
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
requestCount += 1;
expect(request.url.path, '/api/v1/integrations/org-context');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://114-auth.hmac.kr'),
keyId: '',
keySecret: '',
tenantSlug: 'hanmac-family',
);
await client.fetchOrgContext();
await client.fetchOrgContext();
expect(requestCount, 1);
});
test(
'falls back to env org-context credential without session credential',
() async {
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.host, 'env.example.test');
expect(request.url.queryParameters['tenantSlug'], 'env-family');
expect(request.headers['X-Baron-Key-ID'], 'env-key-id');
expect(request.headers['X-Baron-Key-Secret'], 'env-key-secret');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://env.example.test'),
keyId: 'env-key-id',
keySecret: 'env-key-secret',
tenantSlug: 'env-family',
sessionStore: const AuthSessionStore(),
);
await client.fetchOrgContext();
},
);
test(
'remote directory repository filters org-context employees locally',
() async {
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.queryParameters['tenantSlug'], 'is3');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family',
);
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
final employees = await repository.loadEmployees(
const DirectoryQuery(query: '', tenantSlug: 'is3'),
);
expect(employees.map((employee) => employee.name), ['한승민']);
},
);
test(
'remote directory repository searches selected tenant subtree',
() async {
final client = OrgContextApiClient(
httpClient: MockClient((request) async {
expect(request.url.queryParameters['tenantSlug'], 'center');
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family',
);
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
final employees = await repository.loadEmployees(
const DirectoryQuery(
query: '',
tenantSlug: 'center',
tenantSlugs: ['center', 'is3', 'is2'],
),
);
expect(employees.map((employee) => employee.name), ['박주한']);
},
);
test(
'remote directory repository preserves department memberships for duplicate users',
() async {
final client = OrgContextApiClient(
httpClient: MockClient((_) async {
return _jsonResponse(_orgContextResponse());
}),
baseUri: Uri.parse('https://sadmin.hmac.kr'),
keyId: 'key-id',
keySecret: 'key-secret',
tenantSlug: 'hanmac-family',
);
final repository = RemoteDirectoryRepository(orgContextApiClient: client);
final employees = await repository.loadEmployees(
const DirectoryQuery(query: '', tenantSlug: 'all', department: 'IS3'),
);
expect(employees.map((employee) => employee.name), ['한승민', '김윤재']);
},
);
}
Map<String, dynamic> _orgContextResponse() {
return {
'schemaVersion': 'baron.org-context.v1',
'issuedAt': '2026-07-08T10:07:07.182Z',
'scope': {'tenantId': 'family-id', 'tenantSlug': 'hanmac-family'},
'tree': {
'id': 'family-id',
'type': 'COMPANY_GROUP',
'name': '한맥가족',
'slug': 'hanmac-family',
'memberCount': 0,
'children': [
{
'id': 'center-id',
'type': 'DEPARTMENT',
'name': '총괄기획&기술개발센터',
'slug': 'center',
'parentId': 'family-id',
'memberCount': 0,
},
],
},
'tenants': [
{
'id': 'is3-id',
'type': 'TEAM',
'name': 'IS3',
'slug': 'is3',
'parentId': 'center-id',
'memberCount': 0,
'members': [
{
'id': 'user-han',
'name': '한승민',
'phone': '+821031891514',
'email': 'han@example.com',
'department': 'IS3',
'position': '팀장',
'status': 'active',
},
{
'id': 'user-kim',
'name': '김윤재',
'phone': '01097479838',
'email': 'kim@example.com',
'department': 'IS3',
'position': '연구원',
'status': 'active',
},
],
},
{
'id': 'is2-id',
'type': 'TEAM',
'name': 'IS2',
'slug': 'is2',
'parentId': 'center-id',
'memberCount': 0,
'members': [
{
'id': 'user-park',
'name': '박주한',
'phone': '01089553850',
'email': 'park@example.com',
'department': 'IS2',
'position': '연구원',
'status': 'active',
},
{
'id': 'user-han',
'name': '한승민',
'phone': '01031891514',
'email': 'han@example.com',
'department': 'IS2',
'position': '팀장',
'status': 'active',
},
],
},
],
};
}
http.Response _jsonResponse(Map<String, dynamic> body) {
return http.Response.bytes(
utf8.encode(jsonEncode(body)),
200,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}
@@ -0,0 +1,73 @@
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/features/auth/data/auth_session_store.dart';
import 'package:tdc114plus/src/features/auth/domain/auth_models.dart';
import 'package:tdc114plus/src/features/organization/data/organization_api_client.dart';
void main() {
setUp(() async {
SharedPreferences.setMockInitialValues({});
await const AuthSessionStore().save(
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',
),
),
);
});
test('loads org chart with bearer token', () async {
final client = OrganizationApiClient(
httpClient: MockClient((request) async {
expect(request.headers['authorization'], 'Bearer session-token');
expect(request.url.path, '/api/v1/tdc114plus/organization/orgchart');
return http.Response(
jsonEncode({
'tenants': [
{
'id': 'tenant-uuid',
'name': 'Hanmac',
'slug': 'hanmac',
'type': 'COMPANY',
'memberCount': 1,
'totalMemberCount': 1,
},
],
'employees': [
{
'id': 'user-uuid',
'name': 'User One',
'phoneNumber': '+821012345678',
'tenantId': 'tenant-uuid',
'tenantName': 'Hanmac',
'tenantSlug': 'hanmac',
},
],
'generatedAt': '2026-07-02T12:00:00Z',
'cache': {'source': 'db', 'hit': false, 'ttlSeconds': 0},
}),
200,
);
}),
baseUri: Uri.parse('https://sso.example.test'),
sessionStore: const AuthSessionStore(),
);
final snapshot = await client.getOrgChart();
expect(snapshot.tenants.single.slug, 'hanmac');
expect(snapshot.employees.single.name, 'User One');
});
}