feat: 添加自动化任务(automation_jobs)功能模块
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/features/settings/data/models/automation_job_model.dart';
|
||||
import 'package:social_app/features/settings/data/services/automation_jobs_api.dart';
|
||||
import 'package:social_app/features/settings/presentation/cubits/automation_jobs_cubit.dart';
|
||||
|
||||
class MockAutomationJobsApi extends Mock implements AutomationJobsApi {}
|
||||
|
||||
class FakeAutomationJobUpdateRequest extends Fake
|
||||
implements AutomationJobUpdateRequest {}
|
||||
|
||||
void main() {
|
||||
late AutomationJobsCubit cubit;
|
||||
late MockAutomationJobsApi mockApi;
|
||||
|
||||
final testJob = AutomationJobModel(
|
||||
id: '1',
|
||||
ownerId: 'owner1',
|
||||
title: 'Test Job',
|
||||
scheduleType: 'DAILY',
|
||||
runAt: '08:00:00',
|
||||
timezone: 'UTC',
|
||||
status: 'ACTIVE',
|
||||
isSystem: false,
|
||||
config: AutomationJobConfigModel(
|
||||
inputTemplate: '',
|
||||
enabledTools: const [],
|
||||
context: MessageContextConfigModel(
|
||||
source: 'latest_chat',
|
||||
windowMode: 'day',
|
||||
windowCount: 2,
|
||||
),
|
||||
),
|
||||
nextRunAt: DateTime(2024, 1, 1),
|
||||
createdAt: DateTime(2024, 1, 1),
|
||||
updatedAt: DateTime(2024, 1, 1),
|
||||
);
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(FakeAutomationJobUpdateRequest());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockApi = MockAutomationJobsApi();
|
||||
cubit = AutomationJobsCubit(mockApi);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
group('AutomationJobsCubit', () {
|
||||
test('initial state is correct', () {
|
||||
expect(cubit.state.jobs, isEmpty);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
expect(cubit.state.error, isNull);
|
||||
});
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'loadJobs success emits loading then jobs',
|
||||
build: () {
|
||||
when(() => mockApi.list()).thenAnswer((_) async => [testJob]);
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.loadJobs(),
|
||||
expect: () => [
|
||||
isA<AutomationJobsState>().having(
|
||||
(s) => s.isLoading,
|
||||
'isLoading',
|
||||
true,
|
||||
),
|
||||
isA<AutomationJobsState>()
|
||||
.having((s) => s.isLoading, 'isLoading', false)
|
||||
.having((s) => s.jobs, 'jobs', [testJob]),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'loadJobs failure emits loading then error',
|
||||
build: () {
|
||||
when(() => mockApi.list()).thenThrow(Exception('Network error'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.loadJobs(),
|
||||
expect: () => [
|
||||
isA<AutomationJobsState>().having(
|
||||
(s) => s.isLoading,
|
||||
'isLoading',
|
||||
true,
|
||||
),
|
||||
isA<AutomationJobsState>()
|
||||
.having((s) => s.isLoading, 'isLoading', false)
|
||||
.having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'deleteJob success calls loadJobs to refresh',
|
||||
build: () {
|
||||
when(() => mockApi.delete(any())).thenAnswer((_) async {});
|
||||
when(() => mockApi.list()).thenAnswer((_) async => []);
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.deleteJob('1'),
|
||||
verify: (_) {
|
||||
verify(() => mockApi.delete('1')).called(1);
|
||||
verify(() => mockApi.list()).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'deleteJob failure emits error without refreshing',
|
||||
build: () {
|
||||
when(() => mockApi.delete(any())).thenThrow(Exception('Delete failed'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.deleteJob('1'),
|
||||
expect: () => [
|
||||
isA<AutomationJobsState>().having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(() => mockApi.delete('1')).called(1);
|
||||
verifyNever(() => mockApi.list());
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'updateJobStatus success replaces target job',
|
||||
build: () {
|
||||
when(
|
||||
() => mockApi.update(any(), any()),
|
||||
).thenAnswer((_) async => testJob.copyWith(status: 'disabled'));
|
||||
return cubit;
|
||||
},
|
||||
seed: () => AutomationJobsState(jobs: [testJob]),
|
||||
act: (c) => c.updateJobStatus(id: '1', enabled: false),
|
||||
expect: () => [
|
||||
isA<AutomationJobsState>().having(
|
||||
(s) => s.jobs.first.status,
|
||||
'updated status',
|
||||
'disabled',
|
||||
),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(() => mockApi.update('1', any())).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<AutomationJobsCubit, AutomationJobsState>(
|
||||
'updateJobStatus failure emits error',
|
||||
build: () {
|
||||
when(
|
||||
() => mockApi.update(any(), any()),
|
||||
).thenThrow(Exception('Update failed'));
|
||||
return cubit;
|
||||
},
|
||||
seed: () => AutomationJobsState(jobs: [testJob]),
|
||||
act: (c) => c.updateJobStatus(id: '1', enabled: false),
|
||||
expect: () => [
|
||||
isA<AutomationJobsState>().having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/features/settings/data/models/automation_job_model.dart';
|
||||
import 'package:social_app/features/settings/data/services/automation_jobs_api.dart';
|
||||
import 'package:social_app/features/settings/presentation/cubits/job_detail_cubit.dart';
|
||||
|
||||
class MockAutomationJobsApi extends Mock implements AutomationJobsApi {}
|
||||
|
||||
class FakeAutomationJobUpdateRequest extends Fake
|
||||
implements AutomationJobUpdateRequest {}
|
||||
|
||||
class FakeAutomationJobCreateRequest extends Fake
|
||||
implements AutomationJobCreateRequest {}
|
||||
|
||||
void main() {
|
||||
late JobDetailCubit cubit;
|
||||
late MockAutomationJobsApi mockApi;
|
||||
|
||||
final testJob = AutomationJobModel(
|
||||
id: '1',
|
||||
ownerId: 'owner1',
|
||||
title: 'Test Job',
|
||||
scheduleType: 'DAILY',
|
||||
runAt: '08:00:00',
|
||||
timezone: 'UTC',
|
||||
status: 'ACTIVE',
|
||||
isSystem: false,
|
||||
config: AutomationJobConfigModel(
|
||||
inputTemplate: '',
|
||||
enabledTools: const [],
|
||||
context: MessageContextConfigModel(
|
||||
source: 'latest_chat',
|
||||
windowMode: 'day',
|
||||
windowCount: 2,
|
||||
),
|
||||
),
|
||||
nextRunAt: DateTime(2024, 1, 1),
|
||||
createdAt: DateTime(2024, 1, 1),
|
||||
updatedAt: DateTime(2024, 1, 1),
|
||||
);
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(FakeAutomationJobUpdateRequest());
|
||||
registerFallbackValue(FakeAutomationJobCreateRequest());
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockApi = MockAutomationJobsApi();
|
||||
cubit = JobDetailCubit(mockApi);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
group('JobDetailCubit', () {
|
||||
test('initial state is correct', () {
|
||||
expect(cubit.state.job, isNull);
|
||||
expect(cubit.state.isLoading, isFalse);
|
||||
expect(cubit.state.isSaving, isFalse);
|
||||
expect(cubit.state.error, isNull);
|
||||
});
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'loadJob success emits loading then job',
|
||||
build: () {
|
||||
when(() => mockApi.get(any())).thenAnswer((_) async => testJob);
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.loadJob('1'),
|
||||
expect: () => [
|
||||
isA<JobDetailState>().having((s) => s.isLoading, 'isLoading', true),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isLoading, 'isLoading', false)
|
||||
.having((s) => s.job, 'job', testJob),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'loadJob failure emits loading then error',
|
||||
build: () {
|
||||
when(() => mockApi.get(any())).thenThrow(Exception('Network error'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.loadJob('1'),
|
||||
expect: () => [
|
||||
isA<JobDetailState>().having((s) => s.isLoading, 'isLoading', true),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isLoading, 'isLoading', false)
|
||||
.having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'updateJob success emits saving then job with saving false',
|
||||
build: () {
|
||||
when(
|
||||
() => mockApi.update(any(), any()),
|
||||
).thenAnswer((_) async => testJob);
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.updateJob('1', AutomationJobUpdateRequest()),
|
||||
expect: () => [
|
||||
isA<JobDetailState>().having((s) => s.isSaving, 'isSaving', true),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', false)
|
||||
.having((s) => s.job, 'job', testJob),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'updateJob failure emits saving then error',
|
||||
build: () {
|
||||
when(
|
||||
() => mockApi.update(any(), any()),
|
||||
).thenThrow(Exception('Update failed'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.updateJob('1', AutomationJobUpdateRequest()),
|
||||
expect: () => [
|
||||
isA<JobDetailState>().having((s) => s.isSaving, 'isSaving', true),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', false)
|
||||
.having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'deleteJob success emits saving then saving false',
|
||||
build: () {
|
||||
when(() => mockApi.delete(any())).thenAnswer((_) async {});
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.deleteJob('1'),
|
||||
expect: () => [
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', true)
|
||||
.having((s) => s.error, 'error', isNull),
|
||||
isA<JobDetailState>().having((s) => s.isSaving, 'isSaving', false),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(() => mockApi.delete('1')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'deleteJob failure emits saving then error',
|
||||
build: () {
|
||||
when(() => mockApi.delete(any())).thenThrow(Exception('Delete failed'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.deleteJob('1'),
|
||||
expect: () => [
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', true)
|
||||
.having((s) => s.error, 'error', isNull),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', false)
|
||||
.having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(() => mockApi.delete('1')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'createJob success emits saving then created job',
|
||||
build: () {
|
||||
when(() => mockApi.create(any())).thenAnswer((_) async => testJob);
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.createJob(
|
||||
AutomationJobCreateRequest(
|
||||
title: 'New Job',
|
||||
scheduleType: 'daily',
|
||||
runAt: '08:00:00',
|
||||
timezone: 'Asia/Shanghai',
|
||||
status: 'active',
|
||||
config: AutomationJobConfigModel(
|
||||
inputTemplate: 'hello',
|
||||
enabledTools: const ['memory.write'],
|
||||
context: MessageContextConfigModel(
|
||||
source: 'latest_chat',
|
||||
windowMode: 'day',
|
||||
windowCount: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', true)
|
||||
.having((s) => s.error, 'error', isNull),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', false)
|
||||
.having((s) => s.job, 'job', testJob),
|
||||
],
|
||||
verify: (_) {
|
||||
verify(() => mockApi.create(any())).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
blocTest<JobDetailCubit, JobDetailState>(
|
||||
'createJob failure emits saving then error',
|
||||
build: () {
|
||||
when(() => mockApi.create(any())).thenThrow(Exception('Create failed'));
|
||||
return cubit;
|
||||
},
|
||||
act: (c) => c.createJob(
|
||||
AutomationJobCreateRequest(
|
||||
title: 'New Job',
|
||||
scheduleType: 'daily',
|
||||
runAt: '08:00:00',
|
||||
timezone: 'Asia/Shanghai',
|
||||
status: 'active',
|
||||
config: AutomationJobConfigModel(
|
||||
inputTemplate: 'hello',
|
||||
enabledTools: const ['memory.write'],
|
||||
context: MessageContextConfigModel(
|
||||
source: 'latest_chat',
|
||||
windowMode: 'day',
|
||||
windowCount: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
expect: () => [
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', true)
|
||||
.having((s) => s.error, 'error', isNull),
|
||||
isA<JobDetailState>()
|
||||
.having((s) => s.isSaving, 'isSaving', false)
|
||||
.having((s) => s.error, 'error', isNotNull),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user