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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:tdc114plus/src/core/actions/contact_launcher.dart';
|
||||
|
||||
void main() {
|
||||
test('buildCallUri normalizes separators and keeps country code', () {
|
||||
final uri = buildCallUri('+82 10-1234-5678');
|
||||
|
||||
expect(uri, isNotNull);
|
||||
expect(uri.toString(), 'tel:+821012345678');
|
||||
});
|
||||
|
||||
test('buildSmsUri returns null for empty phone number', () {
|
||||
final uri = buildSmsUri(' - ');
|
||||
|
||||
expect(uri, isNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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/directory/data/directory_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('lists employees with bearer token and query filters', () async {
|
||||
final client = DirectoryApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'Bearer session-token');
|
||||
expect(request.url.path, '/api/v1/tdc114plus/directory/employees');
|
||||
expect(request.url.queryParameters['q'], 'kim');
|
||||
expect(request.url.queryParameters['tenantSlug'], 'hanmac');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'items': [
|
||||
{
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'phoneDisplay': '010-1234-5678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
'status': 'active',
|
||||
},
|
||||
],
|
||||
'limit': 50,
|
||||
'offset': 0,
|
||||
'total': 1,
|
||||
'nextCursor': '',
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://sso.example.test'),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
|
||||
final response = await client.listEmployees(
|
||||
query: 'kim',
|
||||
tenantSlug: 'hanmac',
|
||||
);
|
||||
|
||||
expect(response.total, 1);
|
||||
expect(response.items.single.phoneDisplay, '010-1234-5678');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tdc114plus/src/features/directory/presentation/directory_navigation.dart';
|
||||
import 'package:tdc114plus/src/features/organization/domain/organization_models.dart';
|
||||
|
||||
void main() {
|
||||
const tenants = [
|
||||
TenantSummary(
|
||||
id: 'company-a',
|
||||
name: '총괄기획실/기술',
|
||||
slug: 'hq-tech',
|
||||
type: 'COMPANY',
|
||||
memberCount: 10,
|
||||
totalMemberCount: 50,
|
||||
),
|
||||
TenantSummary(
|
||||
id: 'company-b',
|
||||
name: '바른컨설턴트',
|
||||
slug: 'consulting',
|
||||
type: 'COMPANY',
|
||||
memberCount: 5,
|
||||
totalMemberCount: 20,
|
||||
),
|
||||
TenantSummary(
|
||||
id: 'team-is3',
|
||||
name: 'IS3',
|
||||
slug: 'is3',
|
||||
type: 'ORGANIZATION',
|
||||
parentId: 'company-a',
|
||||
memberCount: 3,
|
||||
totalMemberCount: 3,
|
||||
),
|
||||
TenantSummary(
|
||||
id: 'team-ptc',
|
||||
name: 'PTC',
|
||||
slug: 'ptc',
|
||||
type: 'ORGANIZATION',
|
||||
parentId: 'company-a',
|
||||
memberCount: 2,
|
||||
totalMemberCount: 2,
|
||||
),
|
||||
];
|
||||
|
||||
test('shows pinned team chip for selected company', () {
|
||||
final layout = buildTenantNavigationModel(
|
||||
tenants: tenants,
|
||||
selectedTenantSlug: 'hq-tech',
|
||||
pinnedTenantSlug: 'is3',
|
||||
);
|
||||
|
||||
expect(layout.companyTenants.map((tenant) => tenant.slug), [
|
||||
'hq-tech',
|
||||
'consulting',
|
||||
]);
|
||||
expect(layout.visibleChips.map((tenant) => tenant.slug), [
|
||||
'hq-tech',
|
||||
'consulting',
|
||||
'is3',
|
||||
]);
|
||||
});
|
||||
|
||||
test('builds breadcrumb and child tenants for selected child team', () {
|
||||
final layout = buildTenantNavigationModel(
|
||||
tenants: tenants,
|
||||
selectedTenantSlug: 'is3',
|
||||
pinnedTenantSlug: 'is3',
|
||||
);
|
||||
|
||||
expect(layout.selectedTenant?.slug, 'is3');
|
||||
expect(layout.breadcrumbTenants.map((tenant) => tenant.slug), [
|
||||
'hq-tech',
|
||||
'is3',
|
||||
]);
|
||||
expect(layout.childTenants, isEmpty);
|
||||
});
|
||||
|
||||
test('shows company drilldown for all selection', () {
|
||||
final layout = buildTenantNavigationModel(
|
||||
tenants: tenants,
|
||||
selectedTenantSlug: 'all',
|
||||
pinnedTenantSlug: 'is3',
|
||||
);
|
||||
|
||||
expect(layout.selectedTenant, isNull);
|
||||
expect(layout.childTenants.map((tenant) => tenant.slug), [
|
||||
'hq-tech',
|
||||
'consulting',
|
||||
]);
|
||||
expect(layout.visibleChips.map((tenant) => tenant.slug), [
|
||||
'hq-tech',
|
||||
'consulting',
|
||||
'is3',
|
||||
]);
|
||||
});
|
||||
|
||||
test('builds selected tenant subtree slugs for scoped search', () {
|
||||
final layout = buildTenantNavigationModel(
|
||||
tenants: tenants,
|
||||
selectedTenantSlug: 'hq-tech',
|
||||
pinnedTenantSlug: 'is3',
|
||||
);
|
||||
|
||||
expect(layout.selectedTenantSubtreeSlugs, ['hq-tech', 'is3', 'ptc']);
|
||||
});
|
||||
|
||||
test('resolves pinned team slug from company and department name', () {
|
||||
final slug = resolvePinnedTenantSlug(
|
||||
tenants: tenants,
|
||||
companySlug: 'hq-tech',
|
||||
departmentName: 'IS3',
|
||||
);
|
||||
|
||||
expect(slug, 'is3');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:tdc114plus/src/features/directory/data/directory_repository.dart';
|
||||
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
|
||||
|
||||
void main() {
|
||||
test('DirectoryQuery maps all tenant filter to nullable API parameters', () {
|
||||
const query = DirectoryQuery(query: ' 김하늘 ', tenantSlug: 'all');
|
||||
|
||||
expect(query.apiQuery, '김하늘');
|
||||
expect(query.apiTenantSlug, isNull);
|
||||
});
|
||||
|
||||
test('DirectoryQuery keeps selected tenant slug for API requests', () {
|
||||
const query = DirectoryQuery(query: '', tenantSlug: 'hanmac');
|
||||
|
||||
expect(query.apiQuery, isNull);
|
||||
expect(query.apiTenantSlug, 'hanmac');
|
||||
});
|
||||
|
||||
test('DirectoryRepository now focuses on employee lists only', () {
|
||||
const employees = [
|
||||
Employee(
|
||||
id: 'user-001',
|
||||
name: '김하늘',
|
||||
phoneNumber: '+821012345678',
|
||||
tenantId: 'tenant-hanmac',
|
||||
tenantName: '한맥',
|
||||
tenantSlug: 'hanmac',
|
||||
),
|
||||
];
|
||||
|
||||
expect(employees.single.name, '김하늘');
|
||||
expect(employees.single.tenantSlug, 'hanmac');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
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/directory/data/profile_image_api_client.dart';
|
||||
import 'package:tdc114plus/src/features/directory/domain/employee.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('keeps explicit profileImageUrl without auth lookup', () async {
|
||||
var requestCount = 0;
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount += 1;
|
||||
return http.Response('{}', 500);
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'user-1',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
email: 'khkang@samaneng.com',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
profileImageUrl: 'https://cdn.example.test/khkang.jpg',
|
||||
);
|
||||
|
||||
final imageUrl = await client.resolveImageUrl(employee);
|
||||
|
||||
expect(imageUrl, 'https://cdn.example.test/khkang.jpg');
|
||||
expect(requestCount, 0);
|
||||
});
|
||||
|
||||
test('uses auth profile-image endpoint when explicit profile image is absent', () 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-1',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.host, '114-auth.hmac.kr');
|
||||
expect(request.url.path, '/api/v1/profile-image');
|
||||
expect(request.url.queryParameters['comp'], 'SAMAN');
|
||||
expect(request.url.queryParameters['email'], 'khkang@samaneng.com');
|
||||
expect(request.headers['Authorization'], 'Bearer app-session-token');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'found': true,
|
||||
'source': 'BARON_UUID_R2',
|
||||
'imageUrl':
|
||||
'https://baroncs.co.kr/employee_img/bebb832c-052e-493d-b5a9-732518d67685.jpg',
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: store,
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'bebb832c-052e-493d-b5a9-732518d67685',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
email: 'khkang@samaneng.com',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
);
|
||||
|
||||
final imageUrl = await client.resolveImageUrl(employee);
|
||||
|
||||
expect(
|
||||
imageUrl,
|
||||
'https://baroncs.co.kr/employee_img/bebb832c-052e-493d-b5a9-732518d67685.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns null when employee email is missing', () async {
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
fail('auth lookup should not be sent without email');
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'user-2',
|
||||
name: '직원',
|
||||
phoneNumber: '01000000000',
|
||||
tenantId: 'tenant-2',
|
||||
tenantName: '한맥',
|
||||
tenantSlug: 'hanmac',
|
||||
);
|
||||
|
||||
expect(await client.resolveImageUrl(employee), isNull);
|
||||
});
|
||||
|
||||
test('falls back to public uuid image when employee email is missing', () async {
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
fail('auth lookup should not be sent without email');
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: const AuthSessionStore(),
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'bebb832c-052e-493d-b5a9-732518d67685',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
);
|
||||
|
||||
expect(
|
||||
await client.resolveImageUrl(employee),
|
||||
'https://baroncs.co.kr/employee_img/bebb832c-052e-493d-b5a9-732518d67685.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
test('allows auth lookup without company code when tenant mapping is unknown', () 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-3',
|
||||
name: '직원',
|
||||
phoneNumber: '01011112222',
|
||||
tenantId: 'tenant-3',
|
||||
tenantName: 'Unknown',
|
||||
tenantSlug: 'unknown',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.queryParameters['comp'], isNull);
|
||||
expect(request.url.queryParameters['email'], 'person@example.com');
|
||||
return http.Response(
|
||||
jsonEncode({'found': false, 'source': 'DEFAULT'}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: store,
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'user-3',
|
||||
name: '직원',
|
||||
phoneNumber: '01011112222',
|
||||
email: 'person@example.com',
|
||||
tenantId: 'tenant-3',
|
||||
tenantName: 'Unknown',
|
||||
tenantSlug: 'unknown',
|
||||
);
|
||||
|
||||
expect(await client.resolveImageUrl(employee), isNull);
|
||||
});
|
||||
|
||||
test('falls back to public uuid image when auth lookup returns not found', () 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-4',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
return http.Response(
|
||||
jsonEncode({'found': false, 'source': 'DEFAULT'}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: store,
|
||||
);
|
||||
|
||||
const employee = Employee(
|
||||
id: 'bebb832c-052e-493d-b5a9-732518d67685',
|
||||
name: '강경훈',
|
||||
phoneNumber: '01012345678',
|
||||
email: 'khkang@samaneng.com',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
);
|
||||
|
||||
expect(
|
||||
await client.resolveImageUrl(employee),
|
||||
'https://baroncs.co.kr/employee_img/bebb832c-052e-493d-b5a9-732518d67685.jpg',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses uuid fallback even when auth not-found result is cached', () 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-5',
|
||||
name: '직원',
|
||||
phoneNumber: '01012345678',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
var requestCount = 0;
|
||||
final client = ProfileImageApiClient(
|
||||
httpClient: MockClient((request) async {
|
||||
requestCount += 1;
|
||||
return http.Response(
|
||||
jsonEncode({'found': false, 'source': 'DEFAULT'}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}),
|
||||
baseUri: Uri.parse('https://114-auth.hmac.kr'),
|
||||
sessionStore: store,
|
||||
);
|
||||
|
||||
const firstEmployee = Employee(
|
||||
id: 'legacy-user-id',
|
||||
name: '직원',
|
||||
phoneNumber: '01012345678',
|
||||
email: 'cached-null@example.com',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
);
|
||||
const secondEmployee = Employee(
|
||||
id: 'bebb832c-052e-493d-b5a9-732518d67685',
|
||||
name: '직원',
|
||||
phoneNumber: '01012345678',
|
||||
email: 'cached-null@example.com',
|
||||
tenantId: 'tenant-1',
|
||||
tenantName: '삼안',
|
||||
tenantSlug: 'saman',
|
||||
);
|
||||
|
||||
expect(await client.resolveImageUrl(firstEmployee), isNull);
|
||||
expect(
|
||||
await client.resolveImageUrl(secondEmployee),
|
||||
'https://baroncs.co.kr/employee_img/bebb832c-052e-493d-b5a9-732518d67685.jpg',
|
||||
);
|
||||
expect(requestCount, 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:tdc114plus/src/features/favorites/data/favorites_repository.dart';
|
||||
|
||||
void main() {
|
||||
test('loads empty favorites when storage is empty', () async {
|
||||
final repository = SharedPreferencesFavoritesRepository.withStore(
|
||||
_MemoryFavoritesStore(),
|
||||
);
|
||||
|
||||
final favorites = await repository.loadFavorites();
|
||||
|
||||
expect(favorites, isEmpty);
|
||||
});
|
||||
|
||||
test('toggles favorites in local storage', () async {
|
||||
final repository = SharedPreferencesFavoritesRepository.withStore(
|
||||
_MemoryFavoritesStore(),
|
||||
);
|
||||
|
||||
await repository.toggleFavorite('user-001');
|
||||
final added = await repository.loadFavorites();
|
||||
|
||||
expect(added.map((favorite) => favorite.employeeId), ['user-001']);
|
||||
|
||||
await repository.toggleFavorite('user-001');
|
||||
final removed = await repository.loadFavorites();
|
||||
|
||||
expect(removed, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class _MemoryFavoritesStore implements FavoritesKeyValueStore {
|
||||
final _values = <String, String>{};
|
||||
|
||||
@override
|
||||
Future<String?> getString(String key) async {
|
||||
return _values[key];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setString(String key, String value) async {
|
||||
_values[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,54 @@ void main() {
|
||||
expect(response.user.tenantSlug, 'hanmac');
|
||||
});
|
||||
|
||||
test('PhoneLoginLinkInitResponse parses pending auth response', () {
|
||||
final response = PhoneLoginLinkInitResponse.fromJson({
|
||||
'status': 'pending',
|
||||
'pendingRef': 'pending-ref',
|
||||
'expiresIn': 180,
|
||||
'interval': 3,
|
||||
'resendAfter': 30,
|
||||
'provider': 'Ory (Kratos/Hydra)',
|
||||
});
|
||||
|
||||
expect(response.pendingRef, 'pending-ref');
|
||||
expect(response.interval, 3);
|
||||
expect(response.provider, 'Ory (Kratos/Hydra)');
|
||||
});
|
||||
|
||||
test('PhoneLoginLinkPollResponse parses completed session', () {
|
||||
final response = PhoneLoginLinkPollResponse.fromJson({
|
||||
'status': 'ok',
|
||||
'session': {
|
||||
'status': 'ok',
|
||||
'token': 'baron-sso-session-token',
|
||||
'expiresAt': '2026-07-02T12:00:00Z',
|
||||
'user': {
|
||||
'id': 'user-uuid',
|
||||
'name': 'User One',
|
||||
'phoneNumber': '+821012345678',
|
||||
'tenantId': 'tenant-uuid',
|
||||
'tenantName': 'Hanmac',
|
||||
'tenantSlug': 'hanmac',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.session?.token, 'baron-sso-session-token');
|
||||
expect(response.isPending, isFalse);
|
||||
});
|
||||
|
||||
test('PhoneLoginLinkPollResponse detects pending response', () {
|
||||
final response = PhoneLoginLinkPollResponse.fromJson({
|
||||
'status': 'pending',
|
||||
'code': 'authorization_pending',
|
||||
'interval': 5,
|
||||
});
|
||||
|
||||
expect(response.isPending, isTrue);
|
||||
expect(response.interval, 5);
|
||||
});
|
||||
|
||||
test('CurrentUser parses permissions and can map to employee', () {
|
||||
final user = CurrentUser.fromJson({
|
||||
'id': 'user-uuid',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
+1515
-13
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user