A helper app for the Gmadrid Natación swimming section club. This is a mono-repo, containing both the frontend and the backend. A BOT helps with the notice board and trainings, by automatically checking the whatsapp official channel and the trainings' files and copying them into the DB.
The backend is based on supabase, a firebase-like service, but open source and self-hostable.
The frontend is a flutter app, with a focus on Android and iOS platforms.
Needed:
- Apple developer program, or at least being part of the team
- Docker for supabase (backend support)
- A firebase account (testLab, push notifications and crash loggings)
- Apple certificates bound to the provisioning profiles (autosign is disabled)
- java-sdk (recommended by using jenv): currently using
temurin64-18.0.2.1
- Flutter
The latest setup working on my machine is:
$ jenv local
17.0.6
$ flutter --version
Flutter 3.19.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 300451adae (9 days ago) • 2024-03-27 21:54:07 -0500
Engine • revision e76c956498
Tools • Dart 3.3.3 • DevTools 2.31.1
$ xcodebuild -version
Xcode 15.1
Build version 15C65
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.5, on macOS 14.0 23A344 darwin-arm64, locale en-ES)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2023.3.6)
[✓] Connected device (3 available)
[✓] Network resources
$ patrol doctor
Patrol doctor:
Patrol CLI version: 2.7.0
Flutter command: flutter
Flutter 3.19.5 • channel stable
Android:
• Program adb found in /Users/xxx/Library/Android/sdk/platform-tools/adb
• Env var $ANDROID_HOME set to /Users/xxx/Library/Android/sdk
iOS / macOS:
• Program xcodebuild found in /usr/bin/xcodebuild
• Program ideviceinstaller found in /opt/homebrew/bin/ideviceinstaller
$ supabase --version
1.110.1
Install supabase cli
brew install supabase/tap/supabase
Follow the installation guide for your OS (MacOS recommented, otherwise you can't develop the iOS version) on https://docs.flutter.dev/get-started/install
I recommend using brew to install it:
brew install flutter
As a last step, in order to see if everything is intalled correctly, run:
flutter doctor
todo
A makefile can help you with the most common tasks. You can see all the available commands by typing:
make list
When setting up the project you may be interested in:
# initializes the supabase project locally (DB, bucket files etc)
make backend-setup
# installing all the dependencies for the frontend
make frontend-setup
# It sets up a pre-commit hook that helps with the tests. See the tests section below for more info.
# Not needed, but recommended.
make setup-pre-commit
Mainly focused on e-2-e tests. Tests are executed with patrol
lib, in place of the default integration_tests
one, in order to have the full control on the device (accepting notification dialogs etc.) when performing remotely.
Tests can be executed locally with a connected device/running simulator, as well as remotely on Firebase Test Lab.
The app uses its own environment defined by assets/.test.env
when compiling and running e-2-e tests. This is especially needed for firebase tests: since they run on real physical devices, no sidecar-backend can be used on the same machine. This means that the app must be able to connect to a real backend, and not to a local one.
On the other hand this gives us the chance of fully testing the app, without having to mock anything.
The need: we want to execute tests with a real backend and users representing the use-case scenario we need (membership level etc).
The problem:
- user and password would be hardcoded in the artifacts running on external devices (firebase etc), which is not a good practice. Even worse test artifact could be published publicly;
- we don't want to generate users while compiling the test artifacts: we should generate the artifacts each time we want to run them otherwise. We want them re-runnable/reusable (more about it test on CI).
The solution:
- A
TestUserBuilder
called by patrol's test interacts with the supabase's backend to create/delete users when needed. This is done at runtime, so the credentials are not hardcoded in the artifacts; - Supabase test env is set up with a RPC function that
TestUserBuilder
can call along with a specific user/pass used only to generate users. no more actions are allowed for the user; - When compiling the test artifacts, the only one user/pass credentials compiled are the ones used to call the testing users' generation. Not fully safe, but relatively;
- Users are created with a uuid as a username: collision is highly improbable.
The user testing framework needed on the supabase's testing environment can be manually set up by using this sql script.
Check if the assets/.test.env
contains the correct values for you: backend url, supabase project id, test accounts path etc.
You simply need to have a connected device (or a running simulator) and run:
make test-local
Some helper make options are available to compile and send the tests to firebase:
make test-flutter-android
make test-flutter-ios
This both compile and send the tests to firebase. You can also compile the tests locally and send them to firebase manually:
make test-build-artifact
When a PR is opened, the tests are built and executed on firebase. The problem:
- due to the (free) limited resources, it'd be better off reusing already-compiled tests locally.
- since test artifacts have test environment credentials compiled, they cannot be shared publicly on this repo (or other public repos).
The solution: github action as well as make test-flutter-android
and make test-flutter-ios
check if the test artifacts for the specific commit are present on a private server. If they are, they are downloaded and used. Otherwise, they are compiled and sent to firebase.
To create a new artifact:
%%{init: {"flowchart": {"htmlLabels": false}} }%%
flowchart TD
build["`Build android + iOS test artifacts`"]
update-build-info-ts["`Update:
dev/tests/artifact_build_timestamp.info`"]
update-build-info-uuid["Generate a UUID and update with it:
dev/tests/artifact_build_uuid.info"]
rsync-dir["`Generate a dir <TS-UUID> on remote`"]
rsync-connect["`Connect to the remote server by using the make setup-env-test`"]
rsync-files["`RSync the artifact test files listed on:
dev/tests/artifact_files_android.txt
dev/tests/artifact_files_ios.txt`"]
created["remote server now with test build artifact on <remote-root>/<timestamp>-<UUID>"]
build --> update-build-info-ts --> update-build-info-uuid --> rsync-dir --> rsync-connect --> rsync-files --> created
To execute a test by exploiting already build artifacts:
%%{init: {"flowchart": {"htmlLabels": false}} }%%
flowchart TD
check-last-commit{"`check git diff
dev/tests/artifact_build_timestamp.info
between HEAD and HEAD~1`"}
check-last-commit-- changed and newer TS-->check-rsync-artifacts
check-rsync-artifacts{"`Check on remote if exists:
timestamp-uuid
dir
based on dev/test/*.info content`"}
check-rsync-artifacts-- exists --> pull-rsync
check-rsync-artifacts-- does not exist --> build-artifacts
check-last-commit-- unchanged or older TS -->build-artifacts
pull-rsync["Pull the artifacts from the server root `timestamp-uuid/**/*`"]
check-all-artifacts-pulled{"`Check if ALL artifacts are now present
based on:
artifacts_files_android.info
artifacts_files_ios.info`"}
pull-rsync --> check-all-artifacts-pulled
check-all-artifacts-pulled -- they all exist --> execute-test
check-all-artifacts-pulled -- some does not exist --> build-artifacts
build-artifacts["Build artifacts locally"]
execute-test["Execute the tests by using the artifacts"]
build-artifacts --> execute-test
# it compiles the tests and sends them to a private server
make test-artifacts-push
# they will check if we have the latest tests on local, otherwise they will build it
make tests-flutter-android
make tests-flutter-ios
In order to make this work, you need to set up the private server info by running:
make setup-pre-commit
To compile and push the test artifacts you can run:
make push-test-artifacts
In order to generate and push the test build artifacts you can use the pre-commit hook. For each commit:
- it builds the test artifacts
- it pushes them to the private server
make setup-pre-commit
🧐 Hint If you are in a hurry, you can skip the pre-commit hook by using the flag:
git commit --no-verify
but keep in mind that the PR will have to build them, by using CI resources.
The DB is tested with pgTAP.
make test backend
All the tests are under supabase/tests/database
Frontend (Firebase's tests) as well as backend (locally) are executed automatically by github actions when a PR is opened.
In order to save CPU times on github actions (especially the iOS one), a pre-commit git hook can help to build locally the test builds' to be executed. This way, github actions' will simply have to call firebase's test lab, without having to compile the app.
To enable it:
make setup-pre-commit-template
Once you have it, you can use it by simply committing. Keep in mind that you will have to wait for the builds to be generated, so it will take some time.
If you are in a hurry, you can skip the pre-commit hook by using the flag:
git commit --no-verify
I've tried to adopt the best of DDD: SOLID principles, Hexagonal, Domain Events, Aggregates, Value Objects. The code follows a hexagonal architecture, with a clear separation between the domain and the infrastructure. Eny piece of information that is being used or modelled is described by the domain. Any action passes through the Bounded Context.
basically, you have two main independent pieces talking to each other:
- The Bounded Context: managing all the information and the actions related to the domain. It's the core of the app. You read its domain, and you are aware of everything going on in the app;
- The app itself, which is simply an interface for the Bounded Context. It interacts with the Bounded Context by calling the application services, and the Bounded Context interacts with it by emitting events on a shared infrastructure event bus: the app will listen to them and changes accordingly.
This repository follows the Hexagonal Architecture pattern, and it's monorepo. Also, it's structured using submodules
.
├── dev
│ ├── doc
│ └── tests
│ └── env
├── integration_test
│ ├── infrastructure
│ ├── models
│ └── screenshots
├── lib
│ ├── Context
│ │ ├── Natacion
│ │ └── Shared
│ ├── app
│ │ └── screens
│ ├── conf
│ └── shared
│ ├── domain
│ └── infrastructure
├── supabase
│ ├── buckets-init
│ │ └── trainings
│ ├── functions
│ ├── migrations
│ └── tests
│ └── database
Matteo Bertamini www.bertamini.net