dart
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Mobile App CI / test (push) Waiting to run
Mobile App CI / build-android (push) Blocked by required conditions
Mobile App CI / build-ios (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
Deploy Telegram Bot / Deploy Telegram Bot (push) Waiting to run
This commit is contained in:
parent
f9207e6d0c
commit
2eaa6a9067
9 changed files with 365 additions and 0 deletions
25
mnemo_cards_admin/web/Dockerfile
Normal file
25
mnemo_cards_admin/web/Dockerfile
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Build stage ------------------------------------------------------------
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Runtime stage ----------------------------------------------------------
|
||||||
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Replace default config with SPA-aware config
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
COPY --from=build /app/dist/ ./
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1
|
||||||
20
mnemo_cards_backend/.dockerignore
Normal file
20
mnemo_cards_backend/.dockerignore
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
.git
|
||||||
|
.gitmodules
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.cursor
|
||||||
|
build
|
||||||
|
linux/build
|
||||||
|
windows
|
||||||
|
ios
|
||||||
|
android
|
||||||
|
macos
|
||||||
|
test
|
||||||
|
.dart_tool
|
||||||
|
pubspec.lock
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
isar/*.isar
|
||||||
|
isar/*.lock
|
||||||
|
**/.DS_Store
|
||||||
21
mnemo_cards_backend/lib/packs/voice_model_extension.dart
Normal file
21
mnemo_cards_backend/lib/packs/voice_model_extension.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
|
|
||||||
|
extension VoiceModelExtension on VoiceModel {
|
||||||
|
VoiceDto toDto({String? url}) => VoiceDto(
|
||||||
|
id: id ?? -1,
|
||||||
|
phrase: phrase,
|
||||||
|
path: path,
|
||||||
|
speaker: speaker,
|
||||||
|
url: url,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VoiceDtoExtension on VoiceDto {
|
||||||
|
VoiceModel toModel() => VoiceModel(
|
||||||
|
id: id < 0 ? null : id,
|
||||||
|
phrase: phrase,
|
||||||
|
path: path,
|
||||||
|
speaker: speaker,
|
||||||
|
);
|
||||||
|
}
|
||||||
27
mnemo_cards_common/lib/src/dtos/voice_dto.dart
Normal file
27
mnemo_cards_common/lib/src/dtos/voice_dto.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'voice_dto.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
|
@CopyWith()
|
||||||
|
class VoiceDto {
|
||||||
|
final int id;
|
||||||
|
final String phrase;
|
||||||
|
final String path;
|
||||||
|
final String speaker;
|
||||||
|
final String? url;
|
||||||
|
|
||||||
|
const VoiceDto({
|
||||||
|
required this.id,
|
||||||
|
required this.phrase,
|
||||||
|
required this.path,
|
||||||
|
required this.speaker,
|
||||||
|
this.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory VoiceDto.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$VoiceDtoFromJson(json);
|
||||||
|
|
||||||
|
Map<String, Object?> toJson() => _$VoiceDtoToJson(this);
|
||||||
|
}
|
||||||
120
mnemo_cards_common/lib/src/dtos/voice_dto.g.dart
Normal file
120
mnemo_cards_common/lib/src/dtos/voice_dto.g.dart
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'voice_dto.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// CopyWithGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
abstract class _$VoiceDtoCWProxy {
|
||||||
|
VoiceDto id(int id);
|
||||||
|
|
||||||
|
VoiceDto phrase(String phrase);
|
||||||
|
|
||||||
|
VoiceDto path(String path);
|
||||||
|
|
||||||
|
VoiceDto speaker(String speaker);
|
||||||
|
|
||||||
|
VoiceDto url(String? url);
|
||||||
|
|
||||||
|
/// This function **does support** nullification of nullable fields. All `null`
|
||||||
|
/// values passed to `non-nullable` fields will be ignored. You can also use
|
||||||
|
/// `VoiceDto(...).copyWith.fieldName(...)` to override fields one at a time
|
||||||
|
/// with nullification support.
|
||||||
|
///
|
||||||
|
/// Usage
|
||||||
|
/// ```dart
|
||||||
|
/// VoiceDto(...).copyWith(id: 12, name: "My name")
|
||||||
|
/// ````
|
||||||
|
VoiceDto call({
|
||||||
|
int? id,
|
||||||
|
String? phrase,
|
||||||
|
String? path,
|
||||||
|
String? speaker,
|
||||||
|
String? url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Proxy class for `copyWith` functionality. This is a callable class and can
|
||||||
|
/// be used as follows: `instanceOfVoiceDto.copyWith(...)`. Additionally contains
|
||||||
|
/// functions for specific fields e.g. `instanceOfVoiceDto.copyWith.fieldName(...)`
|
||||||
|
class _$VoiceDtoCWProxyImpl implements _$VoiceDtoCWProxy {
|
||||||
|
const _$VoiceDtoCWProxyImpl(this._value);
|
||||||
|
|
||||||
|
final VoiceDto _value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto id(int id) => this(id: id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto phrase(String phrase) => this(phrase: phrase);
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto path(String path) => this(path: path);
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto speaker(String speaker) => this(speaker: speaker);
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto url(String? url) => this(url: url);
|
||||||
|
|
||||||
|
@override
|
||||||
|
VoiceDto call({
|
||||||
|
Object? id = const $CopyWithPlaceholder(),
|
||||||
|
Object? phrase = const $CopyWithPlaceholder(),
|
||||||
|
Object? path = const $CopyWithPlaceholder(),
|
||||||
|
Object? speaker = const $CopyWithPlaceholder(),
|
||||||
|
Object? url = const $CopyWithPlaceholder(),
|
||||||
|
}) {
|
||||||
|
return VoiceDto(
|
||||||
|
id: id == const $CopyWithPlaceholder() || id == null
|
||||||
|
? _value.id
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: id as int,
|
||||||
|
phrase: phrase == const $CopyWithPlaceholder() || phrase == null
|
||||||
|
? _value.phrase
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: phrase as String,
|
||||||
|
path: path == const $CopyWithPlaceholder() || path == null
|
||||||
|
? _value.path
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: path as String,
|
||||||
|
speaker: speaker == const $CopyWithPlaceholder() || speaker == null
|
||||||
|
? _value.speaker
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: speaker as String,
|
||||||
|
url: url == const $CopyWithPlaceholder()
|
||||||
|
? _value.url
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: url as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension $VoiceDtoCopyWith on VoiceDto {
|
||||||
|
/// Returns a callable class that can be used as follows:
|
||||||
|
/// `instanceOfVoiceDto.copyWith(...)` or like so:
|
||||||
|
/// `instanceOfVoiceDto.copyWith.fieldName(...)`.
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_$VoiceDtoCWProxy get copyWith => _$VoiceDtoCWProxyImpl(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
VoiceDto _$VoiceDtoFromJson(Map<String, dynamic> json) => VoiceDto(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
phrase: json['phrase'] as String,
|
||||||
|
path: json['path'] as String,
|
||||||
|
speaker: json['speaker'] as String,
|
||||||
|
url: json['url'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$VoiceDtoToJson(VoiceDto instance) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'phrase': instance.phrase,
|
||||||
|
'path': instance.path,
|
||||||
|
'speaker': instance.speaker,
|
||||||
|
'url': instance.url,
|
||||||
|
};
|
||||||
16
mnemo_cards_web_v2/.dockerignore
Normal file
16
mnemo_cards_web_v2/.dockerignore
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.cursor
|
||||||
|
build
|
||||||
|
linux
|
||||||
|
windows
|
||||||
|
ios
|
||||||
|
android
|
||||||
|
macos
|
||||||
|
.dart_tool
|
||||||
|
pubspec.lock
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
**/.DS_Store
|
||||||
27
mnemo_cards_web_v2/Dockerfile
Normal file
27
mnemo_cards_web_v2/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# --- Build stage ------------------------------------------------------------
|
||||||
|
FROM ghcr.io/cirruslabs/flutter:3.27.4 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pubspec.* ./
|
||||||
|
RUN flutter pub get
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV API_BASE_URL=https://api.mnemo-cards.online
|
||||||
|
|
||||||
|
RUN flutter build web --release \
|
||||||
|
--dart-define=API_BASE_URL=${API_BASE_URL}
|
||||||
|
|
||||||
|
# --- Runtime stage ----------------------------------------------------------
|
||||||
|
FROM nginx:1.27-alpine AS runtime
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
COPY web.nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/build/web/ ./
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/ >/dev/null 2>&1 || exit 1
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ApiConfigV2 voice endpoints', () {
|
||||||
|
test('builds pack card voices path', () {
|
||||||
|
expect(
|
||||||
|
ApiConfigV2.packCardVoices('123', 45),
|
||||||
|
'/packs/123/cards/45/voices',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('builds voice file path and url', () {
|
||||||
|
expect(ApiConfigV2.voiceFile(7), '/voice/7');
|
||||||
|
|
||||||
|
final voiceUrl = ApiConfigV2.getVoiceFileUrl(7);
|
||||||
|
expect(voiceUrl, contains('/api/v2/voice/7'));
|
||||||
|
expect(voiceUrl.startsWith('http'), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/domain/config/api_config_v2.dart';
|
||||||
|
import 'package:mnemo_cards_web_v2/domain/services/http_repository_v2.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class _MockDio extends Mock implements Dio {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late Dio dio;
|
||||||
|
late HttpRepositoryV2 repository;
|
||||||
|
late SharedPreferences prefs;
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(RequestOptions(path: '/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
dio = _MockDio();
|
||||||
|
when(() => dio.interceptors).thenReturn(Interceptors());
|
||||||
|
repository = HttpRepositoryV2(dio: dio, prefs: prefs);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getCardVoices parses response items', () async {
|
||||||
|
final requestPath = ApiConfigV2.packCardVoices('1', 2);
|
||||||
|
final response = Response<Map<String, dynamic>>(
|
||||||
|
data: {
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'phrase': 'hola',
|
||||||
|
'path': 'voice/Lucia/hola.mp3',
|
||||||
|
'speaker': 'Lucia',
|
||||||
|
'url': '/api/v2/voice/1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: requestPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => dio.get<Map<String, dynamic>>(
|
||||||
|
requestPath,
|
||||||
|
data: any(named: 'data'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
queryParameters: any(named: 'queryParameters'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
onReceiveProgress: any(named: 'onReceiveProgress'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => response);
|
||||||
|
|
||||||
|
final result = await repository.getCardVoices('1', 2);
|
||||||
|
|
||||||
|
expect(result, hasLength(1));
|
||||||
|
expect(result.first, isA<VoiceDto>());
|
||||||
|
expect(result.first.phrase, 'hola');
|
||||||
|
expect(result.first.url, '/api/v2/voice/1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getCardVoices returns empty list on 404', () async {
|
||||||
|
when(
|
||||||
|
() => dio.get<Map<String, dynamic>>(
|
||||||
|
any(),
|
||||||
|
data: any(named: 'data'),
|
||||||
|
options: any(named: 'options'),
|
||||||
|
queryParameters: any(named: 'queryParameters'),
|
||||||
|
cancelToken: any(named: 'cancelToken'),
|
||||||
|
onReceiveProgress: any(named: 'onReceiveProgress'),
|
||||||
|
),
|
||||||
|
).thenThrow(
|
||||||
|
DioException(
|
||||||
|
response: Response(
|
||||||
|
statusCode: 404,
|
||||||
|
requestOptions: RequestOptions(path: '/packs/1/cards/2/voices'),
|
||||||
|
),
|
||||||
|
requestOptions: RequestOptions(path: '/packs/1/cards/2/voices'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = await repository.getCardVoices('1', 2);
|
||||||
|
|
||||||
|
expect(result, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue