Test Environment
This file documents the test environment, used to execute the included tests. The test library is documented, in order to re-create this environment for independent verification.
Photos
This test environment uses royalty free pictures from Unsplash. The lowest possible resolution per picture was chosen, in order to keep network traffic low. Additionally, iCloud returns a maximum of 200 records per request: the application splits its queries into ranges using startRank (99 pictures per request for 'All Photos', 66 for albums) and follows the continuationMarker of incomplete ranges. In order to test the split of queries, we need at least 200 pictures in our test environment.
A total of 202 pictures were selected (~17.7MB)
- Photo by Aditya Vyas
- Photo by Jefferson Sees
- Photo by Ian Liberry
- Photo by Aditya Vyas
- Photo by Jefferson Sees
- Photo by Austin Schmid
- Photo by Erwi
- Photo by Mike Cox
- Photo by Leonhard Niederwimmer
- Photo by Ian Liberry
- Photo by Wasa Crispbread
- Photo by Arya Krisdyantara
- Photo by Stephen Leonardi
- Photo by Alsu Vershinina
- Photo by Jeffrey Hamilton
- Photo by Cash Macanaya
- Photo by 2H Media
- Photo by Alex Sh
- Photo by Lana Graves
- Photo by Stephen Leonardi
- Photo by Kateryna Hliznitsova
- Photo by Kate Laine
- Photo by Kevin Staub
- Photo by Elisa Photography
- Photo by Albina White
- Photo by Jefferson Sees
- Photo by Stephen Leonardi
- Photo by Komarov Egor
- Photo by Clay Banks
- Photo by Jason Hudson
- Photo by Hybrid Storytellers
- Photo by Matt Benson
- Photo by Claudio Schwarz
- Photo by Philbo
- Photo by Wasa Crispbread
- Photo by Bruno van der Kraan
- Photo by Jocelyn Morales
- Photo by Sekwang Chia
- Photo by Albina White
- Photo by Bruno van der Kraan
- Photo by Sergey Vinogradov
- Photo by Julien Riedel
- Photo by Chandri Anggara
- Photo by ZHENYU LUO
- Photo by Clay Banks
- Photo by aytam zaker
- Photo by Jefferson Sees
- Photo by Jocelyn Morales
- Photo by Jason Hudson
- Photo by Valery Sysoev
- Photo by Mike Hindle
- Photo by Sekwang Chia
- Photo by Bruno van der Kraan
- Photo by Clark Van Der Beken
- Photo by Les Argonautes
- Photo by ayumi kubo
- Photo by ZHENYU LUO
- Photo by eberhard grossgasteiger
- Photo by Андрей Курган
- Photo by Markus Spiske
- Photo by Clay Banks
- Photo by aytam zaker
- Photo by Tata Bovanenko
- Photo by Sekwang Chia
- Photo by Danial Igdery
- Photo by German Lopez
- Photo by Mathias Reding
- Photo by Bruno van der Kraan
- Photo by Kate Glotova
- Photo by pouria seirafi
- Photo by Christopher Campbell
- Photo by Roman Melnychuk
- Photo by Pierre Lemos
- Photo by aytam zaker
- Photo by Leandra Rieger
- Photo by Mohammadreza alidoost
- Photo by German Lopez
- Photo by Mathias Reding
- Photo by Lorren & Loki
- Photo by Emmanuel Appiah
- Photo by pouria seirafi
- Photo by Zach Camp
- Photo by Sophia Richards
- Photo by Elias Maurer
- Photo by Bruno van der Kraan
- Photo by Svitlana
- Photo by Spencer Marsh
- Photo by Toni Frost
- Photo by Marek Piwnicki
- Photo by German Lopez
- Photo by German Lopez
- Photo by Mathias Reding
- Photo by Zach Camp
- Photo by Raphael Wild
- Photo by Tyler Chandler
- Photo by HUA LING
- Photo by Mathias Reding
- Photo by Elias Maurer
- Photo by Andhyka Widariyanto
- Photo by Julien Riedel
- Photo by Jason Leung
- Photo by Sophia Richards
- Photo by Hanna
- Photo by Wolfgang Hasselmann
- Photo by Emmanuel Appiah
- Photo by Kamil Molendys
- Photo by Lorren & Loki
- Photo by Anthony Ievlev
- Photo by Raphael Wild
- Photo by Solen Feyissa
- Photo by Mathias Reding
- Photo by Zach Camp
- Photo by Mathias Reding
- Photo by Haydon Curteis-Lateo
- Photo by Toni Frost
- Photo by Erik Mclean
- Photo by Christopher Campbell
- Photo by Markus Spiske
- Photo by Hoi An Photographer
- Photo by Junseong Lee
- Photo by Bilal Mansuri
- Photo by D koi
- Photo by Winston Tjia
- Photo by Chandri Anggara
- Photo by Andrea De Santis
- Photo by Emmanuel Appiah
- Photo by Shawn Rain
- Photo by Vimal S
- Photo by Svitlana
- Photo by Honbike
- Photo by Syaoran 7
- Photo by Danist Soh
- Photo by Pawel Czerwinski
- Photo by Ernest Karchmit
- Photo by Steve Johnson
- Photo by Svitlana
- Photo by Honbike
- Photo by Vitaliy Shevchenko
- Photo by Syaoran 7
- Photo by Danist Soh
- Photo by Lydia Lögga
- Photo by Pawel Czerwinski
- Photo by Janosch Diggelmann
- Photo by Ernest Karchmit
- Photo by Steve Johnson
- Photo by Ali Kazal
- Photo by micheile dot com
- Photo by Kateryna Hliznitsova
- Photo by Ozkan Guner
- Photo by Svitlana
- Photo by Honbike
- Photo by Enbymutant
- Photo by Vitaliy Shevchenko
- Photo by Bruno van der Kraan
- Photo by Syaoran 7
- Photo by Parrish Freeman
- Photo by Danist Soh
- Photo by Isaac Martin
- Photo by Lydia Lögga
- Photo by Pawel Czerwinski
- Photo by Ilia Bronskiy
- Photo by Janosch Diggelmann
- Photo by Agata Ciosek
- Photo by Sufyan
- Photo by Rémy Penet
- Photo by Tao Yuan
- Photo by Maxim Berg
- Photo by Hamza NOUASRIA
- Photo by 2H Media
- Photo by Ernest Karchmit
- Photo by Jorgen Hendriksen
- Photo by Jonas Denil
- Photo by Valentin Lacoste
- Photo by Steve Johnson
- Photo by eberhard grossgasteiger
- Photo by Ali Kazal
- Photo by Agata Ciosek
- Photo by Ryunosuke Kikuno
- Photo by Dima Solomin
- Photo by micheile dot com
- Photo by Ben Iwara
- Photo by Kateryna Hliznitsova
- Photo by Chuko Cribb
- Photo by Ozkan Guner
- Photo by Svitlana
- Photo by Pierre Lemos
- Photo by Jinn W
- Photo by Maksym Tymchyk
- Photo by Honbike
- Photo by Red Zeppelin
- Photo by Kate Kasiutich
- Photo by Jack Dong
- Photo by Markus Winkler
- Photo by Brad West
- Photo by Frank Ching
- Photo by Francesco Ungaro
- Photo by Danist Soh
- Photo by Jorge Simmons-Valenzuela
- Photo by Lydia Lögga
- Photo by Pawel Czerwinski
- Photo by Ilia Bronskiy
- Photo by Marek Piwnicki
Folder structure
The following folder structure was created. Underscores indicate that this entry is an Album (instead of a Folder).
├── Memories
│ ├── _2015 - 2016_
│ └── 2022
│ ├── _Summer fun_
│ ├── _Spring/Autumn_
│ └── _Winter sadness_
├── _Random_
└── Stuff
Asset folder relation
All pictures are put into every album, besides Summer fun, which will stay empty. The folder Stuff contains no albums.
Favorites
The first 10 pictures are marked as 'Favorite'.
Edits
The first two and last two pictures will be cropped and 'one-touch' edited.
Limitations
The test library has no hidden photos and no shared library. The API tests therefore only cover the primary library.
Test Suites
The tests are located in app/test/ and executed through Jest (see Build & Test for the commands):
| Suite | Command | Description |
|---|---|---|
test/unit/ |
npm run test:unit |
Unit tests, mocking all network access |
test/api/ |
npm run test:api |
Tests against the real iCloud backend, using the test account. The expected API responses are stored in app/test/api/_data/ |
test/docker/ |
npm run test:docker |
Tests of the Docker image, using testcontainers. npm run test:docker:unit runs the subset that requires no credentials |
Environment
The API and Docker tests read the test account from the following environment variables (see secrets/test.env.sample):
TEST_APPLE_ID_USER: The Apple ID of the test accountTEST_APPLE_ID_PWD: The password of the test accountTEST_TRUST_TOKEN: A valid trust token of the test account, in order to sign in without MFA
The Docker tests run against the image in IMAGE_NAME (default: steilerdev/icloud-photos-sync:nightly). In order to test local changes, build an image first (see Docker Image):
cd app/
npm run build && npm run dist && npm pack
mv icloud-photos-sync-*.tgz ../docker/npm-pack.tgz
docker build -t icps:local ../docker
set -a; . ../secrets/test.env; set +a; IMAGE_NAME=icps:local npm run test:docker
Trust Token
Trust tokens expire after roughly 30 days, and are bound to the IP address they were acquired from (a token is rejected from another network, just like an expired one). Several tokens can co-exist for the same account, so every machine acquires its own token. An expired token shows up as the sign-in requiring MFA, failing the API and Docker tests during authentication. A new token can be acquired with the token command, using a scratch data directory:
APPLE_ID_USER=<test user> APPLE_ID_PWD=<test password> npm run execute -- token -d "$(mktemp -d)" -P 8080
Once it reports that an MFA code is required, request the code through the web UI or its API and submit it (e.g. curl -X POST "localhost:8080/api/mfa?code=<code>"). The trusted devices of the test account don't receive the code, so request it via SMS (curl -X POST "localhost:8080/api/resend_mfa?method=sms&phoneNumberId=2"). The new token is printed, and can be stored as TEST_TRUST_TOKEN.
CI
Tests are executed by .github/workflows/event_pr.yml, depending on the base branch of the pull request:
| Base branch | Tests |
|---|---|
dev |
Unit tests (Ubuntu), Docker unit tests |
beta |
Unit tests (Ubuntu), Docker (E2E) tests, API tests |
main |
Unit tests (Ubuntu and macOS), Docker (E2E) tests, API tests |
Additionally, .github/workflows/monitor_api.yml runs the API tests twice a day (05:20 and 17:20 UTC), in order to detect changes of the iCloud API.
The E2E and API tests run on a self-hosted runner with the residential label, since trust tokens are bound to an IP address and GitHub-hosted runners change their IP address on every run. The test account's credentials and trust token are stored in the runner's environment. Once the token expired, it needs to be renewed on the runner host, using .github/acquire-trust-token.sh.
Writing Tests
- Import the Jest globals explicitly from
@jest/globals. - Use backtick strings as titles in nested
describeblocks.test.each/describe.eachtake object rows with adescfield, used as$descin the title. - Call
prepareResources()(test/_helpers/_general.ts) inbeforeEach, in order to get fresh, typed mocks of the resource singletons:instances.manager: set configuration options directly (e.g._resources.maxRetries = 4), resource file I/O is stubbed.instances.network.mock: anHttpMock(test/_helpers/http-mock.helper.ts), mirroring theaxios-mock-adapterAPI, that throws on unmatched requests.instances.event.spyOnEvent(<event>): a spy on an event, removing existing listeners by default.
- Mock methods by reassigning them with a typed
jest.fn, e.g.obj.method = jest.fn<typeof obj.method>().mockResolvedValue(...). - Use
test/_helpers/mock-fs.helper.tsfor file system tests. It mirrors themock-fsAPI, but writes to a real temporary directory (only paths underos.tmpdir()are allowed). RoundmtimeMsin assertions, since real file systems report sub-millisecond times. - Test the web server with
node-mocks-httpandsendMockedRequest(test/_helpers/web.helper.ts), and the UI withjsdomand@testing-library/dom. - Coverage is always collected. There is no threshold, but new code is expected to come with tests.