1
0
mirror of https://github.com/immich-app/immich.git synced 2025-03-11 15:09:45 +02:00

Merge branch 'main' into refactor/mobile-user-entity

This commit is contained in:
Alex 2025-03-10 21:48:24 -05:00 committed by GitHub
commit 8e17ae0817
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
111 changed files with 852 additions and 1066 deletions

View File

@ -4,7 +4,7 @@ FROM ${BASEIMAGE}
# Flutter SDK
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
ENV FLUTTER_CHANNEL="stable"
ENV FLUTTER_VERSION="3.24.5"
ENV FLUTTER_VERSION="3.29.1"
ENV FLUTTER_HOME=/flutter
ENV PATH=${PATH}:${FLUTTER_HOME}/bin

10
cli/package-lock.json generated
View File

@ -27,7 +27,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^3.0.0",
@ -62,7 +62,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"typescript": "^5.3.3"
}
},
@ -1502,9 +1502,9 @@
}
},
"node_modules/@types/node": {
"version": "22.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz",
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -21,7 +21,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^3.0.0",

12
e2e/package-lock.json generated
View File

@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@ -67,7 +67,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^3.0.0",
@ -102,7 +102,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"typescript": "^5.3.3"
}
},
@ -1717,9 +1717,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz",
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",

View File

@ -24,7 +24,7 @@ WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y --no-install-recommends g++
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:562193a4a9d398f8aedddcb223e583da394ee735de36b5815f8f1d22cb49be15 /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \

View File

@ -1,3 +1,3 @@
{
"flutter": "3.24.5"
"flutter": "3.29.1"
}

1
mobile/.gitignore vendored
View File

@ -56,3 +56,4 @@ libisar.so
# FVM Version
.fvm/
app/

View File

@ -71,7 +71,9 @@ class ImportRule extends DartLintRule {
final path = resolver.path.substring(_rootOffset);
if ((_allowed != null && _allowed!.matches(path)) &&
(_forbidden == null || !_forbidden!.matches(path))) return;
(_forbidden == null || !_forbidden!.matches(path))) {
return;
}
context.registry.addImportDirective((node) {
final uri = node.uri.stringValue;

View File

@ -5,23 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev"
source: hosted
version: "73.0.0"
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.3.3"
analyzer:
dependency: "direct main"
description:
name: analyzer
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev"
source: hosted
version: "6.8.0"
version: "6.11.0"
analyzer_plugin:
dependency: "direct main"
description:
@ -34,26 +34,26 @@ packages:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.6.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
checked_yaml:
dependency: transitive
description:
@ -74,82 +74,82 @@ packages:
dependency: transitive
description:
name: cli_util
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.1"
version: "0.4.2"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
custom_lint:
dependency: transitive
description:
name: custom_lint
sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0"
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545"
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.6.10"
custom_lint_builder:
dependency: "direct main"
description:
name: custom_lint_builder
sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78"
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.6.10"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6"
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.10"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
url: "https://pub.dev"
source: hosted
version: "2.3.7"
version: "2.3.8"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
freezed_annotation:
dependency: transitive
description:
@ -162,18 +162,18 @@ packages:
dependency: "direct main"
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.2.0"
version: "4.3.0"
json_annotation:
dependency: transitive
description:
@ -186,74 +186,74 @@ packages:
dependency: "direct dev"
description:
name: lints
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.16.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.5.0"
rxdart:
dependency: transitive
description:
@ -266,10 +266,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.10.1"
sprintf:
dependency: transitive
description:
@ -282,89 +282,89 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.5.1"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.dev"
source: hosted
version: "14.2.5"
version: "15.0.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.6.0 <4.0.0"

View File

@ -3,7 +3,6 @@ PODS:
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
@ -43,7 +42,7 @@ PODS:
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- flutter_native_splash (2.4.3):
- Flutter
- flutter_udid (0.0.1):
- Flutter
@ -52,7 +51,6 @@ PODS:
- Flutter
- fluttertoast (0.0.2):
- Flutter
- Toast
- geolocator_apple (1.2.0):
- Flutter
- image_picker_ios (0.0.1):
@ -61,19 +59,16 @@ PODS:
- Flutter
- isar_flutter_libs (1.0.0):
- Flutter
- MapLibre (5.14.0-pre3)
- MapLibre (6.5.0)
- maplibre_gl (0.0.1):
- Flutter
- MapLibre (= 5.14.0-pre3)
- MapLibre (= 6.5.0)
- native_video_player (1.0.0):
- Flutter
- network_info_plus (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- path_provider_ios (0.0.1):
- Flutter
- permission_handler_apple (9.3.0):
@ -82,9 +77,9 @@ PODS:
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@ -98,11 +93,10 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- Toast (4.1.1)
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@ -110,7 +104,7 @@ PODS:
DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
@ -127,7 +121,6 @@ DEPENDENCIES:
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
@ -135,7 +128,7 @@ DEPENDENCIES:
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -147,13 +140,12 @@ SPEC REPOS:
- SAMKeychain
- SDWebImage
- SwiftyGif
- Toast
EXTERNAL SOURCES:
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/darwin"
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
@ -186,8 +178,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/network_info_plus/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
@ -202,50 +192,48 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
background_downloader: 3ca0e156ad83a9fc1c8300f5f7c38e94e2d0bf51
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef
maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9
MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e
maplibre_gl: be7b98f1c3ed75bf77f321eec04df359d0ff6f62
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
PODFILE CHECKSUM: 03b7eead4ee77b9e778179eeb0f3b5513617451c

View File

@ -7,7 +7,7 @@
import Flutter
import BackgroundTasks
import path_provider_foundation
import path_provider_ios
import CryptoKit
import Network

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
@ -40,25 +41,29 @@ extension MapMarkers on MapLibreMapController {
await addGeoJSONSourceForMarkers(markers);
await addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
const CircleLayerProperties(
circleRadius: 10,
circleColor: "rgba(150,86,34,0.7)",
circleBlur: 1.0,
circleOpacity: 0.7,
circleStrokeWidth: 0.1,
circleStrokeColor: "rgba(203,46,19,0.5)",
circleStrokeOpacity: 0.7,
),
);
if (Platform.isAndroid) {
await addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
const CircleLayerProperties(
circleRadius: 10,
circleColor: "rgba(150,86,34,0.7)",
circleBlur: 1.0,
circleOpacity: 0.7,
circleStrokeWidth: 0.1,
circleStrokeColor: "rgba(203,46,19,0.5)",
circleStrokeOpacity: 0.7,
),
);
}
// await addHeatmapLayer(
// MapUtils.defaultSourceId,
// MapUtils.defaultHeatMapLayerId,
// MapUtils.defaultHeatMapLayerProperties,
// );
if (Platform.isIOS) {
await addHeatmapLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatMapLayerProperties,
);
}
_completer.complete();
}

View File

@ -10,14 +10,14 @@ extension ImmichColorSchemeExtensions on ColorScheme {
extension ColorExtensions on Color {
Color lighten({double amount = 0.1}) {
return Color.alphaBlend(
Colors.white.withOpacity(amount),
Colors.white.withValues(alpha: amount),
this,
);
}
Color darken({double amount = 0.1}) {
return Color.alphaBlend(
Colors.black.withOpacity(amount),
Colors.black.withValues(alpha: amount),
this,
);
}

View File

@ -6,6 +6,7 @@ typedef AsyncFuture<T> = Future<AsyncValue<T>>;
mixin ErrorLoggerMixin {
abstract final Logger logger;
// ignore: unintended_html_in_doc_comment
/// Returns an AsyncValue<T> if the future is successfully executed
/// Else, logs the error to the overridden logger and returns an AsyncError<>
AsyncFuture<T> guardError<T>(

View File

@ -53,7 +53,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withOpacity(0.15),
backgroundColor: context.primaryColor.withValues(alpha: 0.15),
label: Text(
user.name,
style: const TextStyle(

View File

@ -72,7 +72,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Chip(
backgroundColor: context.primaryColor.withOpacity(0.15),
backgroundColor: context.primaryColor.withValues(alpha: 0.15),
label: Text(
user.email,
style: const TextStyle(

View File

@ -106,9 +106,9 @@ class AlbumsPage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withOpacity(0.075),
context.colorScheme.primary.withOpacity(0.09),
context.colorScheme.primary.withOpacity(0.075),
context.colorScheme.primary.withValues(alpha: 0.075),
context.colorScheme.primary.withValues(alpha: 0.09),
context.colorScheme.primary.withValues(alpha: 0.075),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,

View File

@ -49,9 +49,9 @@ class AppLogPage extends HookConsumerWidget {
Color getTileColor(LogLevel level) => switch (level) {
LogLevel.info => Colors.transparent,
LogLevel.severe => Colors.redAccent.withOpacity(0.25),
LogLevel.warning => Colors.orangeAccent.withOpacity(0.25),
_ => context.primaryColor.withOpacity(0.1),
LogLevel.severe => Colors.redAccent.withValues(alpha: 0.25),
LogLevel.warning => Colors.orangeAccent.withValues(alpha: 0.25),
_ => context.primaryColor.withValues(alpha: 0.1),
};
return Scaffold(

View File

@ -127,7 +127,7 @@ class EditImagePage extends ConsumerWidget {
borderRadius: BorderRadius.circular(7),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
spreadRadius: 2,
blurRadius: 10,
offset: const Offset(0, 3),

View File

@ -49,7 +49,7 @@ class PeopleCollectionPage extends HookConsumerWidget {
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(left: 24),
filled: true,
fillColor: context.primaryColor.withOpacity(0.1),
fillColor: context.primaryColor.withValues(alpha: 0.1),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurfaceSecondary,
),

View File

@ -58,7 +58,8 @@ class SharedLinkPage extends HookConsumerWidget {
child: Icon(
Icons.link_off,
size: 100,
color: context.themeData.iconTheme.color?.withOpacity(0.5),
color:
context.themeData.iconTheme.color?.withValues(alpha: 0.5),
),
),
),

View File

@ -120,7 +120,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
fontSize: 14,
),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5)),
),
),
onTapOutside: (_) => descriptionFocusNode.unfocus(),
@ -146,7 +146,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
fontSize: 14,
),
disabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)),
borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5)),
),
),
);

View File

@ -350,7 +350,7 @@ class MemoryPage extends HookConsumerWidget {
);
},
shape: const CircleBorder(),
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.close_rounded,

View File

@ -517,8 +517,6 @@ class SearchPage extends HookConsumerWidget {
return Icons.abc_rounded;
case TextSearchType.description:
return Icons.text_snippet_outlined;
default:
return Icons.search_rounded;
}
}
@ -634,9 +632,9 @@ class SearchPage extends HookConsumerWidget {
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
colors: [
context.colorScheme.primary.withOpacity(0.075),
context.colorScheme.primary.withOpacity(0.09),
context.colorScheme.primary.withOpacity(0.075),
context.colorScheme.primary.withValues(alpha: 0.075),
context.colorScheme.primary.withValues(alpha: 0.09),
context.colorScheme.primary.withValues(alpha: 0.075),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,

View File

@ -5,6 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity.provider.g.dart';
// ignore: unintended_html_in_doc_comment
/// Maintains the current list of all activities for <share-album-id, asset>
@riverpod
class AlbumActivity extends _$AlbumActivity {

View File

@ -3,6 +3,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_statistics.provider.g.dart';
// ignore: unintended_html_in_doc_comment
/// Maintains the current number of comments by <shared-album, asset>
@riverpod
class ActivityStatistics extends _$ActivityStatistics {

View File

@ -24,9 +24,8 @@ ThemeData getThemeData({
hintColor: colorScheme.onSurfaceSecondary,
focusColor: colorScheme.primary,
scaffoldBackgroundColor: colorScheme.surface,
splashColor: colorScheme.primary.withOpacity(0.1),
highlightColor: colorScheme.primary.withOpacity(0.1),
dialogBackgroundColor: colorScheme.surfaceContainer,
splashColor: colorScheme.primary.withValues(alpha: 0.1),
highlightColor: colorScheme.primary.withValues(alpha: 0.1),
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: colorScheme.surfaceContainer,
),
@ -163,6 +162,7 @@ ThemeData getThemeData({
),
),
),
dialogTheme: DialogThemeData(backgroundColor: colorScheme.surfaceContainer),
);
}

View File

@ -63,7 +63,7 @@ class _ActivityTitle extends StatelessWidget {
Widget build(BuildContext context) {
final textColor = context.isDarkTheme ? Colors.white : Colors.black;
final textStyle = context.textTheme.bodyMedium
?.copyWith(color: textColor.withOpacity(0.6));
?.copyWith(color: textColor.withValues(alpha: 0.6));
return Row(
mainAxisAlignment:

View File

@ -202,12 +202,12 @@ class ThumbnailImage extends ConsumerWidget {
bottom: 5,
child: Icon(
storageIcon(asset),
color: Colors.white.withOpacity(.8),
color: Colors.white.withValues(alpha: .8),
size: 16,
shadows: [
Shadow(
blurRadius: 5.0,
color: Colors.black.withOpacity(0.6),
color: Colors.black.withValues(alpha: 0.6),
offset: const Offset(0.0, 0.0),
),
],

View File

@ -113,7 +113,7 @@ class GalleryAppBar extends ConsumerWidget {
duration: const Duration(milliseconds: 100),
opacity: showControls ? 1.0 : 0.0,
child: Container(
color: Colors.black.withOpacity(0.4),
color: Colors.black.withValues(alpha: 0.4),
child: TopControlAppBar(
isOwner: isOwner,
isPartner: isPartner,

View File

@ -170,7 +170,7 @@ class AppBarServerInfo extends HookConsumerWidget {
child: Tooltip(
verticalOffset: 0,
decoration: BoxDecoration(
color: context.primaryColor.withOpacity(0.9),
color: context.primaryColor.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(

View File

@ -146,7 +146,7 @@ class DropdownSearchMenu<T> extends HookWidget {
? Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.12)
.withValues(alpha: 0.12)
: null,
padding: const EdgeInsets.all(16.0),
child: Text(

View File

@ -124,7 +124,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
decoration: BoxDecoration(
color: badgeBackground,
border: Border.all(
color: context.colorScheme.outline.withOpacity(.3),
color: context.colorScheme.outline.withValues(alpha: .3),
),
borderRadius: BorderRadius.circular(widgetSize / 2),
),

View File

@ -43,7 +43,7 @@ class ImmichToast {
borderRadius: BorderRadius.circular(5.0),
color: context.colorScheme.surfaceContainer,
border: Border.all(
color: context.colorScheme.outline.withOpacity(.5),
color: context.colorScheme.outline.withValues(alpha: .5),
width: 1,
),
),

View File

@ -27,7 +27,8 @@ class ScaffoldErrorBody extends StatelessWidget {
child: Icon(
Icons.error_outline,
size: 100,
color: context.themeData.iconTheme.color?.withOpacity(0.5),
color:
context.themeData.iconTheme.color?.withValues(alpha: 0.5),
),
),
),

View File

@ -48,7 +48,7 @@ class MemoryBottomInfo extends StatelessWidget {
.scrollToDate(memory.assets[0].fileCreatedAt);
},
shape: const CircleBorder(),
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
elevation: 0,
child: const Icon(
Icons.open_in_new,

View File

@ -126,7 +126,7 @@ class _BlurredBackdrop extends HookWidget {
),
),
child: Container(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
),
);
} else {
@ -147,7 +147,7 @@ class _BlurredBackdrop extends HookWidget {
),
),
child: Container(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
),
),
);

View File

@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget {
elevation: 2,
backgroundColor: Colors.black,
overlayColor: WidgetStateProperty.all(
Colors.white.withOpacity(0.1),
Colors.white.withValues(alpha: 0.1),
),
onTap: (memoryIndex) {
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
@ -84,7 +84,7 @@ class MemoryCard extends ConsumerWidget {
children: [
ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withOpacity(0.2),
Colors.black.withValues(alpha: 0.2),
BlendMode.darken,
),
child: Hero(

View File

@ -37,7 +37,7 @@ class PeoplePicker extends HookConsumerWidget {
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(left: 24),
filled: true,
fillColor: context.primaryColor.withOpacity(0.1),
fillColor: context.primaryColor.withValues(alpha: 0.1),
hintStyle: context.textTheme.bodyLarge?.copyWith(
color: context.themeData.colorScheme.onSurfaceSecondary,
),

View File

@ -22,7 +22,7 @@ class SearchFilterChip extends StatelessWidget {
onTap: onTap,
child: Card(
elevation: 0,
color: context.primaryColor.withOpacity(.5),
color: context.primaryColor.withValues(alpha: .5),
shape: StadiumBorder(
side: BorderSide(color: context.colorScheme.secondaryContainer),
),

View File

@ -44,8 +44,8 @@ class ThumbnailWithInfoContainer extends StatelessWidget {
colors: [
Colors.transparent,
label == ''
? Colors.black.withOpacity(0.1)
: Colors.black.withOpacity(0.5),
? Colors.black.withValues(alpha: 0.1)
: Colors.black.withValues(alpha: 0.5),
],
stops: const [0.0, 1.0],
),

View File

@ -72,7 +72,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
builder: (BuildContext context, Widget? child) {
return Material(
color: context.colorScheme.surfaceContainerHighest,
shadowColor: context.colorScheme.primary.withOpacity(0.2),
shadowColor: context.colorScheme.primary.withValues(alpha: 0.2),
child: child,
);
},
@ -116,7 +116,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
child: Icon(
Icons.dns_rounded,
size: 120,
color: context.primaryColor.withOpacity(0.05),
color: context.primaryColor.withValues(alpha: 0.05),
),
),
ListView(

View File

@ -161,7 +161,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
child: Icon(
Icons.home_outlined,
size: 120,
color: context.primaryColor.withOpacity(0.05),
color: context.primaryColor.withValues(alpha: 0.05),
),
),
ListView(

View File

@ -98,7 +98,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(100)),
color: Colors.grey[900]?.withOpacity(.4),
color: Colors.grey[900]?.withValues(alpha: .4),
),
child: const Padding(
padding: EdgeInsets.all(3),

View File

@ -240,7 +240,7 @@ class SharedLinkItem extends ConsumerWidget {
child: Tooltip(
verticalOffset: 0,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.9),
color: colorScheme.primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(
@ -268,7 +268,7 @@ class SharedLinkItem extends ConsumerWidget {
child: Tooltip(
verticalOffset: 0,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.9),
color: colorScheme.primary.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(10),
),
textStyle: TextStyle(

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ version: 1.129.0+187
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: 3.24.5
flutter: 3.29.1
isar_version: &isar_version 3.1.8 # define the version to be used
@ -14,7 +14,6 @@ dependencies:
flutter:
sdk: flutter
path_provider_ios:
photo_manager: ^3.6.1
photo_manager_image_provider: ^2.2.0
flutter_hooks: ^0.21.2
@ -39,7 +38,8 @@ dependencies:
flutter_displaymode: ^0.6.0
scrollable_positioned_list: ^0.3.8
path: ^1.8.3
path_provider: ^2.1.2
path_provider: ^2.1.5
path_provider_ios: ^2.0.11
collection: ^1.18.0
http_parser: ^4.0.2
flutter_web_auth_2: ^5.0.0-alpha.0

View File

@ -12,7 +12,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"typescript": "^5.3.3"
}
},
@ -23,9 +23,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz",
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"typescript": "^5.3.3"
},
"repository": {

View File

@ -17,7 +17,6 @@
"@nestjs/platform-socket.io": "^11.0.4",
"@nestjs/schedule": "^5.0.0",
"@nestjs/swagger": "^11.0.2",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.4",
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
"@opentelemetry/context-async-hooks": "^1.24.0",
@ -89,7 +88,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",
@ -2920,19 +2919,6 @@
}
}
},
"node_modules/@nestjs/typeorm": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz",
"integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@nestjs/core": "^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.2.0",
"typeorm": "^0.3.0"
}
},
"node_modules/@nestjs/websockets": {
"version": "11.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.11.tgz",
@ -5830,9 +5816,9 @@
}
},
"node_modules/@types/node": {
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"

View File

@ -43,7 +43,6 @@
"@nestjs/platform-socket.io": "^11.0.4",
"@nestjs/schedule": "^5.0.0",
"@nestjs/swagger": "^11.0.2",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.4",
"@opentelemetry/auto-instrumentations-node": "^0.56.0",
"@opentelemetry/context-async-hooks": "^1.24.0",
@ -115,7 +114,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",

View File

@ -1,11 +1,12 @@
import { LoggingRepository } from 'src/repositories/logging.repository';
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
import { automock } from 'test/utils';
describe(NotificationRepository.name, () => {
let sut: NotificationRepository;
beforeEach(() => {
sut = new NotificationRepository(newFakeLoggingRepository());
sut = new NotificationRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }));
});
describe('renderEmail', () => {

View File

@ -1,7 +1,8 @@
import mockfs from 'mock-fs';
import { CrawlOptionsDto } from 'src/dtos/library.dto';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
import { automock } from 'test/utils';
interface Test {
test: string;
@ -182,7 +183,7 @@ describe(StorageRepository.name, () => {
let sut: StorageRepository;
beforeEach(() => {
sut = new StorageRepository(newFakeLoggingRepository());
sut = new StorageRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }));
});
afterEach(() => {

View File

@ -146,6 +146,7 @@ describe(ActivityService.name, () => {
const activity = factory.activity();
mocks.access.activity.checkOwnerAccess.mockResolvedValue(new Set([activity.id]));
mocks.activity.delete.mockResolvedValue();
await sut.delete(factory.auth(), activity.id);
@ -156,6 +157,7 @@ describe(ActivityService.name, () => {
const activity = factory.activity();
mocks.access.activity.checkAlbumOwnerAccess.mockResolvedValue(new Set([activity.id]));
mocks.activity.delete.mockResolvedValue();
await sut.delete(factory.auth(), activity.id);

View File

@ -347,6 +347,7 @@ describe(AlbumService.name, () => {
it('should remove a shared user from an owned album', async () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id]));
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
mocks.albumUser.delete.mockResolvedValue();
await expect(
sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id),
@ -376,6 +377,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves', async () => {
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
mocks.albumUser.delete.mockResolvedValue();
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id);
@ -388,6 +390,7 @@ describe(AlbumService.name, () => {
it('should allow a shared user to remove themselves using "me"', async () => {
mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser);
mocks.albumUser.delete.mockResolvedValue();
await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me');
@ -422,6 +425,8 @@ describe(AlbumService.name, () => {
describe('updateUser', () => {
it('should update user role', async () => {
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
mocks.albumUser.update.mockResolvedValue(null as any);
await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
role: AlbumUserRole.EDITOR,
});

View File

@ -67,6 +67,8 @@ describe(ApiKeyService.name, () => {
const id = newUuid();
const auth = factory.auth();
mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.update(auth, id, { name: 'New Name' })).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.apiKey.update).not.toHaveBeenCalledWith(id);
@ -91,6 +93,8 @@ describe(ApiKeyService.name, () => {
const auth = factory.auth();
const id = newUuid();
mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.delete(auth, id)).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.apiKey.delete).not.toHaveBeenCalledWith(id);
@ -101,6 +105,7 @@ describe(ApiKeyService.name, () => {
const apiKey = factory.apiKey({ userId: auth.user.id });
mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.delete.mockResolvedValue();
await sut.delete(auth, apiKey.id);
@ -113,6 +118,8 @@ describe(ApiKeyService.name, () => {
const auth = factory.auth();
const id = newUuid();
mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.getById(auth, id)).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.apiKey.getById).toHaveBeenCalledWith(auth.user.id, id);

View File

@ -127,8 +127,11 @@ describe(AssetService.name, () => {
describe('getRandom', () => {
it('should get own random assets', async () => {
mocks.partner.getAll.mockResolvedValue([]);
mocks.asset.getRandom.mockResolvedValue([assetStub.image]);
await sut.getRandom(authStub.admin, 1);
expect(mocks.asset.getRandom).toHaveBeenCalledWith([authStub.admin.user.id], 1);
});
@ -531,6 +534,7 @@ describe(AssetService.name, () => {
});
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
mocks.stack.update.mockResolvedValue(factory.stack() as unknown as any);
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
@ -542,6 +546,7 @@ describe(AssetService.name, () => {
});
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
mocks.stack.delete.mockResolvedValue();
mocks.asset.getById.mockResolvedValue({
...assetStub.primaryImage,
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },

View File

@ -18,7 +18,10 @@ describe(AuditService.name, () => {
describe('handleCleanup', () => {
it('should delete old audit entries', async () => {
mocks.audit.removeBefore.mockResolvedValue();
await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
});
});

View File

@ -65,7 +65,10 @@ describe('AuthService', () => {
describe('onBootstrap', () => {
it('should init the repo', () => {
mocks.oauth.init.mockResolvedValue();
sut.onBootstrap();
expect(mocks.oauth.init).toHaveBeenCalled();
});
});
@ -73,24 +76,30 @@ describe('AuthService', () => {
describe('login', () => {
it('should throw an error if password login is disabled', async () => {
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.disabled);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
});
it('should check the user exists', async () => {
mocks.user.getByEmail.mockResolvedValue(void 0);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
});
it('should check the user has a password', async () => {
mocks.user.getByEmail.mockResolvedValue({} as UserEntity);
await expect(sut.login(fixtures.login, loginDetails)).rejects.toBeInstanceOf(UnauthorizedException);
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
});
it('should successfully log the user in', async () => {
mocks.user.getByEmail.mockResolvedValue(userStub.user1);
mocks.session.create.mockResolvedValue(sessionStub.valid);
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual({
accessToken: 'cmFuZG9tLWJ5dGVz',
userId: 'user-id',
@ -100,6 +109,7 @@ describe('AuthService', () => {
isAdmin: false,
shouldChangePassword: false,
});
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
});
});
@ -159,8 +169,10 @@ describe('AuthService', () => {
describe('logout', () => {
it('should return the end session endpoint', async () => {
const auth = factory.auth();
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
const auth = { user: { id: '123' } } as AuthDto;
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({
successful: true,
redirectUri: 'http://end-session-endpoint',
@ -168,7 +180,7 @@ describe('AuthService', () => {
});
it('should return the default redirect', async () => {
const auth = { user: { id: '123' } } as AuthDto;
const auth = factory.auth();
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
successful: true,
@ -178,6 +190,7 @@ describe('AuthService', () => {
it('should delete the access token', async () => {
const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto;
mocks.session.delete.mockResolvedValue();
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({
successful: true,
@ -203,7 +216,9 @@ describe('AuthService', () => {
it('should only allow one admin', async () => {
mocks.user.getAdmin.mockResolvedValue({} as UserEntity);
await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.user.getAdmin).toHaveBeenCalled();
});
@ -215,6 +230,7 @@ describe('AuthService', () => {
createdAt: new Date('2021-01-01'),
metadata: [] as UserMetadataEntity[],
} as UserEntity);
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({
avatarColor: expect.any(String),
id: 'admin',
@ -222,6 +238,7 @@ describe('AuthService', () => {
email: 'test@immich.com',
name: 'immich admin',
});
expect(mocks.user.getAdmin).toHaveBeenCalled();
expect(mocks.user.create).toHaveBeenCalled();
});
@ -241,6 +258,7 @@ describe('AuthService', () => {
it('should validate using authorization header', async () => {
mocks.user.get.mockResolvedValue(userStub.user1);
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { authorization: 'Bearer auth_token' },
@ -256,6 +274,8 @@ describe('AuthService', () => {
describe('validate - shared key', () => {
it('should not accept a non-existent key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(void 0);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': 'key' },
@ -267,6 +287,7 @@ describe('AuthService', () => {
it('should not accept an expired key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': 'key' },
@ -278,6 +299,7 @@ describe('AuthService', () => {
it('should not accept a key on a non-shared route', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': 'key' },
@ -290,6 +312,7 @@ describe('AuthService', () => {
it('should not accept a key without a user', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.expired);
mocks.user.get.mockResolvedValue(void 0);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': 'key' },
@ -302,6 +325,7 @@ describe('AuthService', () => {
it('should accept a base64url key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
mocks.user.get.mockResolvedValue(userStub.admin);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') },
@ -318,6 +342,7 @@ describe('AuthService', () => {
it('should accept a hex key', async () => {
mocks.sharedLink.getByKey.mockResolvedValue(sharedLinkStub.valid);
mocks.user.get.mockResolvedValue(userStub.admin);
await expect(
sut.authenticate({
headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') },
@ -335,6 +360,7 @@ describe('AuthService', () => {
describe('validate - user token', () => {
it('should throw if no token is found', async () => {
mocks.session.getByToken.mockResolvedValue(void 0);
await expect(
sut.authenticate({
headers: { 'x-immich-user-token': 'auth_token' },
@ -346,6 +372,7 @@ describe('AuthService', () => {
it('should return an auth dto', async () => {
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
@ -360,6 +387,7 @@ describe('AuthService', () => {
it('should throw if admin route and not an admin', async () => {
mocks.session.getByToken.mockResolvedValue(sessionStub.valid as any);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
@ -372,6 +400,7 @@ describe('AuthService', () => {
it('should update when access time exceeds an hour', async () => {
mocks.session.getByToken.mockResolvedValue(sessionStub.inactive as any);
mocks.session.update.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
@ -386,6 +415,7 @@ describe('AuthService', () => {
describe('validate - api key', () => {
it('should throw an error if no api key is found', async () => {
mocks.apiKey.getKey.mockResolvedValue(void 0);
await expect(
sut.authenticate({
headers: { 'x-api-key': 'auth_token' },
@ -401,6 +431,7 @@ describe('AuthService', () => {
const authApiKey = factory.authApiKey({ permissions: [] });
mocks.apiKey.getKey.mockResolvedValue({ ...authApiKey, user: authUser });
await expect(
sut.authenticate({
headers: { 'x-api-key': 'auth_token' },
@ -442,6 +473,7 @@ describe('AuthService', () => {
describe('authorize', () => {
it('should fail if oauth is disabled', async () => {
mocks.systemMetadata.get.mockResolvedValue({ oauth: { enabled: false } });
await expect(sut.authorize({ redirectUri: 'https://demo.immich.app' })).rejects.toBeInstanceOf(
BadRequestException,
);
@ -449,6 +481,7 @@ describe('AuthService', () => {
it('should authorize the user', async () => {
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithMobileOverride);
await sut.authorize({ redirectUri: 'https://demo.immich.app' });
});
});
@ -461,9 +494,11 @@ describe('AuthService', () => {
it('should not allow auto registering', async () => {
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthEnabled);
mocks.user.getByEmail.mockResolvedValue(void 0);
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).rejects.toBeInstanceOf(
BadRequestException,
);
expect(mocks.user.getByEmail).toHaveBeenCalledTimes(1);
});
@ -540,6 +575,7 @@ describe('AuthService', () => {
mocks.session.create.mockResolvedValue(sessionStub.valid);
await sut.callback({ url }, loginDetails);
expect(mocks.oauth.getProfile).toHaveBeenCalledWith(expect.objectContaining({}), url, 'http://mobile-redirect');
});
}
@ -549,6 +585,7 @@ describe('AuthService', () => {
mocks.user.getByEmail.mockResolvedValue(void 0);
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
@ -563,6 +600,7 @@ describe('AuthService', () => {
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 'abc' });
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
@ -577,6 +615,7 @@ describe('AuthService', () => {
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: -5 });
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
@ -591,6 +630,7 @@ describe('AuthService', () => {
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 0 });
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,
@ -611,6 +651,7 @@ describe('AuthService', () => {
mocks.user.getAdmin.mockResolvedValue(userStub.user1);
mocks.user.create.mockResolvedValue(userStub.user1);
mocks.oauth.getProfile.mockResolvedValue({ sub, email, immich_quota: 5 });
mocks.session.create.mockResolvedValue(factory.session());
await expect(sut.callback({ url: 'http://immich/auth/login?code=abc123' }, loginDetails)).resolves.toEqual(
oauthResponse,

View File

@ -22,6 +22,7 @@ describe(BackupService.name, () => {
describe('onBootstrapEvent', () => {
it('should init cron job and handle config changes', async () => {
mocks.database.tryLock.mockResolvedValue(true);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
@ -47,10 +48,14 @@ describe(BackupService.name, () => {
describe('onConfigUpdateEvent', () => {
beforeEach(async () => {
mocks.database.tryLock.mockResolvedValue(true);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: defaults });
});
it('should update cron job if backup is enabled', () => {
mocks.cron.update.mockResolvedValue();
sut.onConfigUpdate({
oldConfig: defaults,
newConfig: {

View File

@ -31,6 +31,8 @@ describe(CliService.name, () => {
it('should default to a random password', async () => {
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
mocks.user.update.mockResolvedValue(userStub.admin);
const ask = vitest.fn().mockImplementation(() => {});
const response = await sut.resetAdminPassword(ask);
@ -45,6 +47,8 @@ describe(CliService.name, () => {
it('should use the supplied password', async () => {
mocks.user.getAdmin.mockResolvedValue(userStub.admin);
mocks.user.update.mockResolvedValue(userStub.admin);
const ask = vitest.fn().mockResolvedValue('new-password');
const response = await sut.resetAdminPassword(ask);

View File

@ -173,6 +173,7 @@ describe(DownloadService.name, () => {
it('should return a list of archives (assetIds)', async () => {
const assetIds = ['asset-1', 'asset-2'];
mocks.user.getMetadata.mockResolvedValue([]);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
mocks.downloadRepository.downloadAssetIds.mockReturnValue(
makeStream([
@ -187,6 +188,7 @@ describe(DownloadService.name, () => {
});
it('should return a list of archives (albumId)', async () => {
mocks.user.getMetadata.mockResolvedValue([]);
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
mocks.downloadRepository.downloadAlbumId.mockReturnValue(
makeStream([
@ -202,6 +204,7 @@ describe(DownloadService.name, () => {
});
it('should return a list of archives (userId)', async () => {
mocks.user.getMetadata.mockResolvedValue([]);
mocks.downloadRepository.downloadUserId.mockReturnValue(
makeStream([
{ id: 'asset-1', livePhotoVideoId: null, size: 100_000 },
@ -217,6 +220,7 @@ describe(DownloadService.name, () => {
});
it('should split archives by size', async () => {
mocks.user.getMetadata.mockResolvedValue([]);
mocks.downloadRepository.downloadUserId.mockReturnValue(
makeStream([
{ id: 'asset-1', livePhotoVideoId: null, size: 5000 },
@ -244,13 +248,13 @@ describe(DownloadService.name, () => {
const assetIds = ['asset-1', 'asset-2'];
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
mocks.user.getMetadata.mockResolvedValue([]);
mocks.downloadRepository.downloadAssetIds.mockReturnValue(
makeStream([
{ id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 },
{ id: 'asset-2', livePhotoVideoId: 'asset-4', size: 100_000 },
]),
);
mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue(
makeStream([
{ id: 'asset-3', livePhotoVideoId: null, size: 23_456, originalPath: '/path/to/file.mp4' },
@ -271,11 +275,10 @@ describe(DownloadService.name, () => {
const assetIds = ['asset-1'];
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds));
mocks.user.getMetadata.mockResolvedValue([]);
mocks.downloadRepository.downloadAssetIds.mockReturnValue(
makeStream([{ id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }]),
);
mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue(
makeStream([
{ id: 'asset-2', livePhotoVideoId: null, size: 23_456, originalPath: 'upload/encoded-video/uuid-MP.mp4' },

View File

@ -36,6 +36,9 @@ describe(LibraryService.name, () => {
describe('onConfigInit', () => {
it('should init cron job and handle config changes', async () => {
mocks.cron.create.mockResolvedValue();
mocks.cron.update.mockResolvedValue();
await sut.onConfigInit({ newConfig: defaults });
expect(mocks.cron.create).toHaveBeenCalled();
@ -65,6 +68,7 @@ describe(LibraryService.name, () => {
mocks.library.get.mockImplementation((id) =>
Promise.resolve([library1, library2].find((library) => library.id === id)),
);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
@ -74,6 +78,8 @@ describe(LibraryService.name, () => {
});
it('should not initialize watcher when watching is disabled', async () => {
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
expect(mocks.storage.watch).not.toHaveBeenCalled();
@ -99,6 +105,8 @@ describe(LibraryService.name, () => {
describe('onConfigUpdateEvent', () => {
beforeEach(async () => {
mocks.database.tryLock.mockResolvedValue(true);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: defaults });
});
@ -111,6 +119,9 @@ describe(LibraryService.name, () => {
it('should update cron job and enable watching', async () => {
mocks.library.getAll.mockResolvedValue([]);
mocks.cron.create.mockResolvedValue();
mocks.cron.update.mockResolvedValue();
await sut.onConfigUpdate({
newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
oldConfig: defaults,
@ -125,6 +136,9 @@ describe(LibraryService.name, () => {
it('should update cron job and disable watching', async () => {
mocks.library.getAll.mockResolvedValue([]);
mocks.cron.create.mockResolvedValue();
mocks.cron.update.mockResolvedValue();
await sut.onConfigUpdate({
newConfig: systemConfigStub.libraryScanAndWatch as SystemConfig,
oldConfig: defaults,
@ -620,6 +634,7 @@ describe(LibraryService.name, () => {
const mockClose = vitest.fn();
mocks.storage.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
await sut.delete(library.id);
@ -765,6 +780,7 @@ describe(LibraryService.name, () => {
mocks.library.create.mockResolvedValue(library);
mocks.library.get.mockResolvedValue(library);
mocks.library.getAll.mockResolvedValue([]);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
await sut.create({ ownerId: authStub.admin.user.id, importPaths: library.importPaths });
@ -832,6 +848,7 @@ describe(LibraryService.name, () => {
describe('update', () => {
beforeEach(async () => {
mocks.library.getAll.mockResolvedValue([]);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
});
@ -878,6 +895,8 @@ describe(LibraryService.name, () => {
describe('watching disabled', () => {
beforeEach(async () => {
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchDisabled as SystemConfig });
});
@ -895,6 +914,8 @@ describe(LibraryService.name, () => {
describe('watching enabled', () => {
beforeEach(async () => {
mocks.library.getAll.mockResolvedValue([]);
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
});
@ -1067,6 +1088,7 @@ describe(LibraryService.name, () => {
const mockClose = vitest.fn();
mocks.storage.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
mocks.cron.create.mockResolvedValue();
await sut.onConfigInit({ newConfig: systemConfigStub.libraryWatchEnabled as SystemConfig });
await sut.onShutdown();

View File

@ -33,6 +33,8 @@ describe(MemoryService.name, () => {
});
it('should map ', async () => {
mocks.memory.search.mockResolvedValue([]);
await expect(sut.search(factory.auth(), {})).resolves.toEqual([]);
});
});
@ -46,6 +48,7 @@ describe(MemoryService.name, () => {
const [memoryId] = newUuids();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memoryId]));
mocks.memory.get.mockResolvedValue(void 0);
await expect(sut.get(factory.auth(), memoryId)).rejects.toBeInstanceOf(BadRequestException);
});
@ -159,6 +162,7 @@ describe(MemoryService.name, () => {
const memoryId = newUuid();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memoryId]));
mocks.memory.delete.mockResolvedValue();
await expect(sut.remove(factory.auth(), memoryId)).resolves.toBeUndefined();
@ -183,6 +187,7 @@ describe(MemoryService.name, () => {
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.getAssetIds.mockResolvedValue(new Set());
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([
{ error: 'no_permission', id: assetId, success: false },
@ -213,6 +218,9 @@ describe(MemoryService.name, () => {
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
mocks.memory.get.mockResolvedValue(memory);
mocks.memory.update.mockResolvedValue(memory);
mocks.memory.getAssetIds.mockResolvedValue(new Set());
mocks.memory.addAssetIds.mockResolvedValue();
await expect(sut.addAssets(factory.auth(), memory.id, { ids: [assetId] })).resolves.toEqual([
{ id: assetId, success: true },
@ -233,6 +241,7 @@ describe(MemoryService.name, () => {
it('should skip assets not in the memory', async () => {
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
mocks.memory.getAssetIds.mockResolvedValue(new Set());
await expect(sut.removeAssets(factory.auth(), 'memory1', { ids: ['not-found'] })).resolves.toEqual([
{ error: 'not_found', id: 'not-found', success: false },
@ -242,15 +251,20 @@ describe(MemoryService.name, () => {
});
it('should remove assets', async () => {
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1']));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1']));
mocks.memory.getAssetIds.mockResolvedValue(new Set(['asset1']));
const memory = factory.memory();
const asset = factory.asset();
await expect(sut.removeAssets(factory.auth(), 'memory1', { ids: ['asset1'] })).resolves.toEqual([
{ id: 'asset1', success: true },
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.memory.getAssetIds.mockResolvedValue(new Set([asset.id]));
mocks.memory.removeAssetIds.mockResolvedValue();
mocks.memory.update.mockResolvedValue(memory);
await expect(sut.removeAssets(factory.auth(), memory.id, { ids: [asset.id] })).resolves.toEqual([
{ id: asset.id, success: true },
]);
expect(mocks.memory.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']);
expect(mocks.memory.removeAssetIds).toHaveBeenCalledWith(memory.id, [asset.id]);
});
});
});

View File

@ -52,6 +52,10 @@ describe(MetadataService.name, () => {
describe('onBootstrapEvent', () => {
it('should pause and resume queue during init', async () => {
mocks.job.pause.mockResolvedValue();
mocks.map.init.mockResolvedValue();
mocks.job.resume.mockResolvedValue();
await sut.onBootstrap();
expect(mocks.job.pause).toHaveBeenCalledTimes(1);

View File

@ -260,6 +260,7 @@ describe(NotificationService.name, () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
@ -279,6 +280,7 @@ describe(NotificationService.name, () => {
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
expect(mocks.notification.renderEmail).toHaveBeenCalledWith({
@ -297,6 +299,7 @@ describe(NotificationService.name, () => {
mocks.user.get.mockResolvedValue(userStub.admin);
mocks.notification.verifySmtp.mockResolvedValue(true);
mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' });
mocks.notification.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
await expect(
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),

View File

@ -324,6 +324,10 @@ describe(PersonService.name, () => {
mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]);
mocks.person.reassignFace.mockResolvedValue(1);
mocks.person.getRandomFace.mockResolvedValue(faceStub.primaryFace1);
mocks.person.refreshFaces.mockResolvedValue();
mocks.person.reassignFace.mockResolvedValue(5);
mocks.person.update.mockResolvedValue(personStub.noName);
await expect(
sut.reassignFaces(authStub.admin, personStub.noName.id, {
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
@ -515,6 +519,7 @@ describe(PersonService.name, () => {
hasNextPage: false,
});
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
mocks.person.deleteFaces.mockResolvedValue();
await sut.handleQueueDetectFaces({ force: true });
@ -633,6 +638,7 @@ describe(PersonService.name, () => {
mocks.person.getAll.mockReturnValue(makeStream());
mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
mocks.person.getAllWithoutFaces.mockResolvedValue([]);
mocks.person.unassignFaces.mockResolvedValue();
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
@ -679,6 +685,7 @@ describe(PersonService.name, () => {
mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson]));
mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1]));
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
mocks.person.unassignFaces.mockResolvedValue();
await sut.handleQueueRecognizeFaces({ force: true });
@ -757,6 +764,7 @@ describe(PersonService.name, () => {
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
mocks.search.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]);
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
mocks.person.refreshFaces.mockResolvedValue();
await sut.handleDetectFaces({ id: assetStub.image.id });
@ -784,6 +792,7 @@ describe(PersonService.name, () => {
it('should add new face and delete an existing face not among the new detected faces', async () => {
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.primaryFace1] }]);
mocks.person.refreshFaces.mockResolvedValue();
await sut.handleDetectFaces({ id: assetStub.image.id });
@ -799,6 +808,7 @@ describe(PersonService.name, () => {
it('should add embedding to matching metadata face', async () => {
mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock);
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, faces: [faceStub.fromExif1] }]);
mocks.person.refreshFaces.mockResolvedValue();
await sut.handleDetectFaces({ id: assetStub.image.id });
@ -1006,6 +1016,7 @@ describe(PersonService.name, () => {
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle);
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
mocks.media.generateThumbnail.mockResolvedValue();
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
@ -1038,6 +1049,7 @@ describe(PersonService.name, () => {
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId });
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.start);
mocks.asset.getById.mockResolvedValue(assetStub.image);
mocks.media.generateThumbnail.mockResolvedValue();
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });
@ -1063,7 +1075,9 @@ describe(PersonService.name, () => {
it('should generate a thumbnail without overflowing', async () => {
mocks.person.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
mocks.person.getFaceByIdWithAssets.mockResolvedValue(faceStub.end);
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
mocks.media.generateThumbnail.mockResolvedValue();
await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id });

View File

@ -57,6 +57,8 @@ describe(SearchService.name, () => {
describe('getSearchSuggestions', () => {
it('should return search suggestions for country', async () => {
mocks.search.getCountries.mockResolvedValue(['USA']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
).resolves.toEqual(['USA']);
@ -65,6 +67,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for country (including null)', async () => {
mocks.search.getCountries.mockResolvedValue(['USA']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
).resolves.toEqual(['USA', null]);
@ -73,6 +77,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for state', async () => {
mocks.search.getStates.mockResolvedValue(['California']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.STATE }),
).resolves.toEqual(['California']);
@ -81,6 +87,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for state (including null)', async () => {
mocks.search.getStates.mockResolvedValue(['California']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.STATE }),
).resolves.toEqual(['California', null]);
@ -89,6 +97,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for city', async () => {
mocks.search.getCities.mockResolvedValue(['Denver']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CITY }),
).resolves.toEqual(['Denver']);
@ -97,6 +107,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for city (including null)', async () => {
mocks.search.getCities.mockResolvedValue(['Denver']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CITY }),
).resolves.toEqual(['Denver', null]);
@ -105,6 +117,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for camera make', async () => {
mocks.search.getCameraMakes.mockResolvedValue(['Nikon']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MAKE }),
).resolves.toEqual(['Nikon']);
@ -113,6 +127,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for camera make (including null)', async () => {
mocks.search.getCameraMakes.mockResolvedValue(['Nikon']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MAKE }),
).resolves.toEqual(['Nikon', null]);
@ -121,6 +137,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for camera model', async () => {
mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MODEL }),
).resolves.toEqual(['Fujifilm X100VI']);
@ -129,6 +147,8 @@ describe(SearchService.name, () => {
it('should return search suggestions for camera model (including null)', async () => {
mocks.search.getCameraModels.mockResolvedValue(['Fujifilm X100VI']);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MODEL }),
).resolves.toEqual(['Fujifilm X100VI', null]);

View File

@ -36,6 +36,7 @@ describe('SessionService', () => {
updateId: 'uuid-v7',
},
]);
mocks.session.delete.mockResolvedValue();
await expect(sut.handleCleanup()).resolves.toEqual(JobStatus.SUCCESS);
expect(mocks.session.delete).toHaveBeenCalledWith('123');
@ -71,6 +72,7 @@ describe('SessionService', () => {
describe('logoutDevices', () => {
it('should logout all devices', async () => {
mocks.session.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);
mocks.session.delete.mockResolvedValue();
await sut.deleteAll(authStub.user1);
@ -83,6 +85,7 @@ describe('SessionService', () => {
describe('logoutDevice', () => {
it('should logout the device', async () => {
mocks.access.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1']));
mocks.session.delete.mockResolvedValue();
await sut.delete(authStub.user1, 'token-1');

View File

@ -71,7 +71,10 @@ describe(SharedLinkService.name, () => {
describe('get', () => {
it('should throw an error for an invalid shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(void 0);
await expect(sut.get(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
});
@ -194,7 +197,10 @@ describe(SharedLinkService.name, () => {
describe('update', () => {
it('should throw an error for an invalid shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(void 0);
await expect(sut.update(authStub.user1, 'missing-id', {})).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
});
@ -214,14 +220,20 @@ describe(SharedLinkService.name, () => {
describe('remove', () => {
it('should throw an error for an invalid shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(void 0);
await expect(sut.remove(authStub.user1, 'missing-id')).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, 'missing-id');
expect(mocks.sharedLink.update).not.toHaveBeenCalled();
});
it('should remove a key', async () => {
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
mocks.sharedLink.remove.mockResolvedValue();
await sut.remove(authStub.user1, sharedLinkStub.valid.id);
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authStub.user1.user.id, sharedLinkStub.valid.id);
expect(mocks.sharedLink.remove).toHaveBeenCalledWith(sharedLinkStub.valid);
});
@ -238,6 +250,7 @@ describe(SharedLinkService.name, () => {
it('should add assets to a shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual);
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3']));
await expect(
@ -268,6 +281,7 @@ describe(SharedLinkService.name, () => {
it('should remove assets from a shared link', async () => {
mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual));
mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual);
mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual);
await expect(
sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }),

View File

@ -155,6 +155,7 @@ describe(StackService.name, () => {
it('should delete stack', async () => {
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
mocks.stack.delete.mockResolvedValue();
await sut.delete(authStub.admin, 'stack-id');
@ -176,6 +177,7 @@ describe(StackService.name, () => {
it('should delete all stacks', async () => {
mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id']));
mocks.stack.deleteAll.mockResolvedValue();
await sut.deleteAll(authStub.admin, { ids: ['stack-id'] });

View File

@ -93,7 +93,9 @@ describe(StorageTemplateService.name, () => {
describe('handleMigrationSingle', () => {
it('should skip when storage template is disabled', async () => {
mocks.systemMetadata.get.mockResolvedValue({ storageTemplate: { enabled: false } });
await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.SKIPPED);
expect(mocks.asset.getByIds).not.toHaveBeenCalled();
expect(mocks.storage.checkFileExists).not.toHaveBeenCalled();
expect(mocks.storage.rename).not.toHaveBeenCalled();

View File

@ -87,9 +87,12 @@ describe(TagService.name, () => {
it('should create a new tag with optional color', async () => {
mocks.tag.create.mockResolvedValue(tagStub.colorCreate);
mocks.tag.getByValue.mockResolvedValue(void 0);
await expect(sut.create(authStub.admin, { name: 'tag-1', color: '#000000' })).resolves.toEqual(
tagResponseStub.color1,
);
expect(mocks.tag.create).toHaveBeenCalledWith({
userId: authStub.admin.user.id,
value: 'tag-1',
@ -168,6 +171,8 @@ describe(TagService.name, () => {
it('should remove a tag', async () => {
mocks.tag.get.mockResolvedValue(tagStub.tag);
mocks.tag.delete.mockResolvedValue();
await sut.remove(authStub.admin, 'tag-1');
expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1');
});
@ -223,6 +228,7 @@ describe(TagService.name, () => {
it('should accept accept ids that are new and reject the rest', async () => {
mocks.tag.get.mockResolvedValue(tagStub.tag);
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1']));
mocks.tag.addAssetIds.mockResolvedValue();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2']));
await expect(
@ -242,6 +248,8 @@ describe(TagService.name, () => {
describe('removeAssets', () => {
it('should throw an error for an invalid id', async () => {
mocks.tag.getAssetIds.mockResolvedValue(new Set());
mocks.tag.removeAssetIds.mockResolvedValue();
await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([
{ id: 'asset-1', success: false, error: 'not_found' },
]);
@ -250,6 +258,7 @@ describe(TagService.name, () => {
it('should accept accept ids that are tagged and reject the rest', async () => {
mocks.tag.get.mockResolvedValue(tagStub.tag);
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1']));
mocks.tag.removeAssetIds.mockResolvedValue();
await expect(
sut.removeAssets(authStub.admin, 'tag-1', {
@ -267,7 +276,10 @@ describe(TagService.name, () => {
describe('handleTagCleanup', () => {
it('should delete empty tags', async () => {
mocks.tag.deleteEmptyTags.mockResolvedValue();
await expect(sut.handleTagCleanup()).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.tag.deleteEmptyTags).toHaveBeenCalled();
});
});

View File

@ -70,6 +70,7 @@ describe(TimelineService.name, () => {
it('should include partner shared assets', async () => {
mocks.asset.getTimeBucket.mockResolvedValue([assetStub.image]);
mocks.partner.getAll.mockResolvedValue([]);
await expect(
sut.getTimeBucket(authStub.admin, {

View File

@ -39,6 +39,7 @@ describe(TrashService.name, () => {
it('should restore a batch of assets', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2']));
mocks.trash.restoreAll.mockResolvedValue(0);
await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] });

View File

@ -4,6 +4,7 @@ import { serverVersion } from 'src/constants';
import { ImmichEnvironment, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
import { VersionService } from 'src/services/version.service';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { factory } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
const mockRelease = (version: string) => ({
@ -30,7 +31,12 @@ describe(VersionService.name, () => {
describe('onBootstrap', () => {
it('should record a new version', async () => {
mocks.versionHistory.getAll.mockResolvedValue([]);
mocks.versionHistory.getLatest.mockResolvedValue(void 0);
mocks.versionHistory.create.mockResolvedValue(factory.versionHistory());
await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(mocks.versionHistory.create).toHaveBeenCalledWith({ version: expect.any(String) });
});

View File

@ -34,9 +34,9 @@ import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
import { newUuid } from 'test/small.factory';
import { automock } from 'test/utils';
class CustomWritable extends Writable {
private data = '';
@ -213,7 +213,7 @@ export class TestContext {
view: ViewRepository;
private constructor(public db: Kysely<DB>) {
const logger = newLoggingRepositoryMock() as unknown as LoggingRepository;
const logger = automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false });
const config = new ConfigRepository();
this.access = new AccessRepository(this.db);

View File

@ -3,12 +3,14 @@ import { writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AssetEntity } from 'src/entities/asset.entity';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
import { newRandomImage, newTestService, ServiceMocks } from 'test/utils';
import { automock, newRandomImage, newTestService, ServiceMocks } from 'test/utils';
const metadataRepository = new MetadataRepository(newFakeLoggingRepository());
const metadataRepository = new MetadataRepository(
automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }),
);
const createTestFile = async (exifData: Record<string, any>) => {
const data = newRandomImage();

View File

@ -1,12 +0,0 @@
import { ActivityRepository } from 'src/repositories/activity.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newActivityRepositoryMock = (): Mocked<RepositoryInterface<ActivityRepository>> => {
return {
search: vitest.fn(),
create: vitest.fn(),
delete: vitest.fn(),
getStatistics: vitest.fn(),
};
};

View File

@ -1,11 +0,0 @@
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newAlbumUserRepositoryMock = (): Mocked<RepositoryInterface<AlbumUserRepository>> => {
return {
create: vitest.fn(),
delete: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -1,25 +0,0 @@
import { AlbumRepository } from 'src/repositories/album.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newAlbumRepositoryMock = (): Mocked<RepositoryInterface<AlbumRepository>> => {
return {
getById: vitest.fn(),
getByAssetId: vitest.fn(),
getMetadataForIds: vitest.fn(),
getOwned: vitest.fn(),
getShared: vitest.fn(),
getNotShared: vitest.fn(),
restoreAll: vitest.fn(),
softDeleteAll: vitest.fn(),
deleteAll: vitest.fn(),
addAssetIds: vitest.fn(),
removeAsset: vitest.fn(),
removeAssetIds: vitest.fn(),
getAssetIds: vitest.fn(),
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
updateThumbnails: vitest.fn(),
};
};

View File

@ -1,14 +0,0 @@
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newKeyRepositoryMock = (): Mocked<RepositoryInterface<ApiKeyRepository>> => {
return {
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
getKey: vitest.fn(),
getById: vitest.fn(),
getByUserId: vitest.fn(),
};
};

View File

@ -1,10 +0,0 @@
import { AuditRepository } from 'src/repositories/audit.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newAuditRepositoryMock = (): Mocked<RepositoryInterface<AuditRepository>> => {
return {
getAfter: vitest.fn(),
removeBefore: vitest.fn(),
};
};

View File

@ -1,10 +0,0 @@
import { CronRepository } from 'src/repositories/cron.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newCronRepositoryMock = (): Mocked<RepositoryInterface<CronRepository>> => {
return {
create: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -1,12 +0,0 @@
import { DownloadRepository } from 'src/repositories/download.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newDownloadRepositoryMock = (): Mocked<RepositoryInterface<DownloadRepository>> => {
return {
downloadAssetIds: vitest.fn(),
downloadMotionAssetIds: vitest.fn(),
downloadAlbumId: vitest.fn(),
downloadUserId: vitest.fn(),
};
};

View File

@ -1,17 +0,0 @@
import { EventRepository } from 'src/repositories/event.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newEventRepositoryMock = (): Mocked<RepositoryInterface<EventRepository>> => {
return {
setup: vitest.fn(),
emit: vitest.fn() as any,
clientSend: vitest.fn() as any,
clientBroadcast: vitest.fn() as any,
serverSend: vitest.fn(),
afterInit: vitest.fn(),
handleConnection: vitest.fn(),
handleDisconnect: vitest.fn(),
setAuthFn: vitest.fn(),
};
};

View File

@ -1,17 +0,0 @@
import { LibraryRepository } from 'src/repositories/library.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newLibraryRepositoryMock = (): Mocked<RepositoryInterface<LibraryRepository>> => {
return {
get: vitest.fn(),
create: vitest.fn(),
delete: vitest.fn(),
softDelete: vitest.fn(),
update: vitest.fn(),
getStatistics: vitest.fn(),
getAllDeleted: vitest.fn(),
getAll: vitest.fn(),
streamAssetIds: vitest.fn(),
};
};

View File

@ -1,23 +0,0 @@
import { LoggingRepository } from 'src/repositories/logging.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newLoggingRepositoryMock = (): Mocked<RepositoryInterface<LoggingRepository>> => {
return {
setLogLevel: vitest.fn(),
setContext: vitest.fn(),
setAppName: vitest.fn(),
isLevelEnabled: vitest.fn(),
verbose: vitest.fn(),
verboseFn: vitest.fn(),
debug: vitest.fn(),
debugFn: vitest.fn(),
log: vitest.fn(),
warn: vitest.fn(),
error: vitest.fn(),
fatal: vitest.fn(),
};
};
export const newFakeLoggingRepository = () =>
newLoggingRepositoryMock() as RepositoryInterface<LoggingRepository> as LoggingRepository;

View File

@ -1,11 +0,0 @@
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newMachineLearningRepositoryMock = (): Mocked<RepositoryInterface<MachineLearningRepository>> => {
return {
encodeImage: vitest.fn(),
encodeText: vitest.fn(),
detectFaces: vitest.fn(),
};
};

View File

@ -1,11 +0,0 @@
import { MapRepository } from 'src/repositories/map.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newMapRepositoryMock = (): Mocked<RepositoryInterface<MapRepository>> => {
return {
init: vitest.fn(),
reverseGeocode: vitest.fn(),
getMapMarkers: vitest.fn(),
};
};

View File

@ -1,17 +0,0 @@
import { MemoryRepository } from 'src/repositories/memory.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newMemoryRepositoryMock = (): Mocked<RepositoryInterface<MemoryRepository>> => {
return {
search: vitest.fn().mockResolvedValue([]),
get: vitest.fn(),
create: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
getAssetIds: vitest.fn().mockResolvedValue(new Set()),
addAssetIds: vitest.fn(),
removeAssetIds: vitest.fn(),
cleanup: vitest.fn(),
};
};

View File

@ -1,14 +0,0 @@
import { MoveRepository } from 'src/repositories/move.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newMoveRepositoryMock = (): Mocked<RepositoryInterface<MoveRepository>> => {
return {
create: vitest.fn(),
getByEntity: vitest.fn(),
update: vitest.fn(),
delete: vitest.fn(),
cleanMoveHistory: vitest.fn(),
cleanMoveHistorySingle: vitest.fn(),
};
};

View File

@ -1,11 +0,0 @@
import { NotificationRepository } from 'src/repositories/notification.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newNotificationRepositoryMock = (): Mocked<RepositoryInterface<NotificationRepository>> => {
return {
renderEmail: vitest.fn(),
sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }),
verifySmtp: vitest.fn(),
};
};

View File

@ -1,12 +0,0 @@
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked } from 'vitest';
export const newOAuthRepositoryMock = (): Mocked<RepositoryInterface<OAuthRepository>> => {
return {
init: vitest.fn(),
authorize: vitest.fn(),
getLogoutEndpoint: vitest.fn(),
getProfile: vitest.fn(),
};
};

View File

@ -1,13 +0,0 @@
import { PartnerRepository } from 'src/repositories/partner.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newPartnerRepositoryMock = (): Mocked<RepositoryInterface<PartnerRepository>> => {
return {
create: vitest.fn(),
remove: vitest.fn(),
getAll: vitest.fn().mockResolvedValue([]),
get: vitest.fn(),
update: vitest.fn(),
};
};

View File

@ -1,41 +0,0 @@
import { PersonRepository } from 'src/repositories/person.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newPersonRepositoryMock = (): Mocked<RepositoryInterface<PersonRepository>> => {
return {
getById: vitest.fn(),
getAll: vitest.fn(),
getAllForUser: vitest.fn(),
getAllWithoutFaces: vitest.fn(),
getByName: vitest.fn(),
getDistinctNames: vitest.fn(),
create: vitest.fn(),
createAll: vitest.fn(),
update: vitest.fn(),
updateAll: vitest.fn(),
delete: vitest.fn(),
deleteFaces: vitest.fn(),
getStatistics: vitest.fn(),
getAllFaces: vitest.fn(),
getFacesByIds: vitest.fn(),
getRandomFace: vitest.fn(),
reassignFaces: vitest.fn(),
unassignFaces: vitest.fn(),
refreshFaces: vitest.fn(),
getFaces: vitest.fn(),
reassignFace: vitest.fn(),
getFaceById: vitest.fn(),
getFaceByIdWithAssets: vitest.fn(),
getNumberOfPeople: vitest.fn(),
getLatestFaceDate: vitest.fn(),
createAssetFace: vitest.fn(),
deleteAssetFace: vitest.fn(),
softDeleteAssetFaces: vitest.fn(),
};
};

View File

@ -1,9 +0,0 @@
import { ProcessRepository } from 'src/repositories/process.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newProcessRepositoryMock = (): Mocked<RepositoryInterface<ProcessRepository>> => {
return {
spawn: vitest.fn(),
};
};

View File

@ -1,24 +0,0 @@
import { SearchRepository } from 'src/repositories/search.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newSearchRepositoryMock = (): Mocked<RepositoryInterface<SearchRepository>> => {
return {
searchMetadata: vitest.fn(),
searchSmart: vitest.fn(),
searchDuplicates: vitest.fn(),
searchFaces: vitest.fn(),
searchRandom: vitest.fn(),
upsert: vitest.fn(),
searchPlaces: vitest.fn(),
getAssetsByCity: vitest.fn(),
deleteAllSearchEmbeddings: vitest.fn(),
getDimensionSize: vitest.fn(),
setDimensionSize: vitest.fn(),
getCameraMakes: vitest.fn(),
getCameraModels: vitest.fn(),
getCities: vitest.fn(),
getCountries: vitest.fn(),
getStates: vitest.fn(),
};
};

View File

@ -1,10 +0,0 @@
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export const newServerInfoRepositoryMock = (): Mocked<RepositoryInterface<ServerInfoRepository>> => {
return {
getGitHubRelease: vitest.fn(),
getBuildVersions: vitest.fn(),
};
};

Some files were not shown because too many files have changed in this diff Show More