diff --git a/documentation/.gitignore b/documentation/.gitignore index e9d4b99708..0ef98f5f98 100644 --- a/documentation/.gitignore +++ b/documentation/.gitignore @@ -12,6 +12,9 @@ docs/advanced-options/starsky docs/advanced-options/starskydesktop docs/advanced-options/starsky-tools +# show desktop folder +!docs/getting-started/desktop + # the output of the build step build diff --git a/documentation/docs/assets/getting-started-configuration-desktop-open.jpg b/documentation/docs/assets/getting-started-configuration-desktop-open.jpg new file mode 100644 index 0000000000..b3508f6baf Binary files /dev/null and b/documentation/docs/assets/getting-started-configuration-desktop-open.jpg differ diff --git a/documentation/docs/developer-guide/api/readme.md b/documentation/docs/developer-guide/api/readme.md index 8192afde44..7825f2d3f9 100644 --- a/documentation/docs/developer-guide/api/readme.md +++ b/documentation/docs/developer-guide/api/readme.md @@ -30,6 +30,9 @@ This document is auto generated | __/api/delete__ | DELETE| Remove files from the disk, but the file must contain the !delete! (TrashKeyw...| | _Parameters: f (subPaths, separated by dot comma), collections (true is to update files with the same name before _ | | _ the extenstion) _ | +| __/api/desktop-editor/open__ | GET | Open a file in the default editor or a specific editor on the desktop | +| _Parameters: f (single or multiple subPaths), collections (to combine files with the same name before the extension) _ | +| __/api/desktop-editor/amount-confirmation__ | GET | Check the amount of files to open before | | __/api/disk/mkdir__ | POST | Make a directory (-p) | | __/api/disk/rename__ | POST | Rename file/folder and update it in the database | | _Parameters: f (from subPath), to (to subPath), collections (is collections bool), currentStatus (default is to not _ | @@ -103,7 +106,6 @@ This document is auto generated | _ json (text as output), extraLarge (give preference to extraLarge over large image) _ | | __/api/thumbnail/zoom/\{f\}@\{z\}__ | GET | Get zoomed in image by fileHash.At the moment this is the source image | | __/api/thumbnail-generation__ | POST | Create thumbnails for a folder in the background | -| __/api/trash/detect-to-use-system-trash__ | GET | Is the system trash supported | -| __/api/trash/move-to-trash__ | POST | (beta) Move a file to the trash | +| __/api/trash/move-to-trash__ | POST | Move a file to the trash | | __/api/upload__ | POST | Upload to specific folder (does not check if already has been imported)Use th...| | __/api/upload-sidecar__ | POST | Upload sidecar file to specific folder (does not check if already has been im...| diff --git a/documentation/docs/features/bulk-editing.md b/documentation/docs/features/bulk-editing.md index 29680038db..f7f53bebf8 100644 --- a/documentation/docs/features/bulk-editing.md +++ b/documentation/docs/features/bulk-editing.md @@ -1,26 +1,36 @@ # Bulk editing -Bulk editing is a feature that allows you to edit multiple files at once. +Bulk editing is a feature that allows you to edit multiple files at once. Is it possible to edit the metadata for multiple images at once. You can use it in search and archive. In detail view you can edit the metadata for a single image. ## 1. Select the images + Via the search or archive you can select multiple images. Press select and Labels and select the images you want to update. ## Update + When you selected an image you can update the metadata. You can update the following metadata: + - Tags - Info - Title - ColorClass (the color label of the image) via the API you could also update other metadata like: + - Location - Software - etc. # Replace the metadata in the fields: tags, info or title + You can search and replace the metadata in the fields: tags, info or title. -Is easy to undo typos or update the metadata. \ No newline at end of file +Is easy to undo typos or update the metadata. + +# Open files + +When using the application as desktop mode you can batch open files with your favorite editor. +See [Desktop Open](../getting-started/configuration/desktop-open.md) for more information. \ No newline at end of file diff --git a/documentation/docs/features/stacks.md b/documentation/docs/features/stacks.md index 5e7581ea3a..6fff90b613 100644 --- a/documentation/docs/features/stacks.md +++ b/documentation/docs/features/stacks.md @@ -2,13 +2,19 @@ Stacks are enabled by default. Stacks are a way to group photos together. -For example, you might have a photo that is associated with both a JPEG and an RAW file. -In this case, the Stack organizes these two files together so that they are easier to find, manage and use later on. +For example, you might have a photo that is associated with both a JPEG and an RAW file. +In this case, the Stack organizes these two files together so that they are easier to find, manage +and use later on. You may come across pictures that are associated with more than one media file. ## For what reasons can files be stacked? -- Files sharing exactly the same file and folder name will always be stacked together. - - for example `/2018/IMG_1234.jpg` and `/2018/IMG_1234.avi` +- Files sharing exactly the same file and folder name will always be stacked together. + - for example `/2018/IMG_1234.jpg` and `/2018/IMG_1234.avi` +# Open files + +When using the application as desktop mode you can batch open files with your favorite editor. It +will also respect the stack settings. +See [Desktop Open](../getting-started/configuration/desktop-open.md) for more information. diff --git a/documentation/docs/getting-started/configuration/_category_.json b/documentation/docs/getting-started/configuration/_category_.json new file mode 100644 index 0000000000..f92373a37a --- /dev/null +++ b/documentation/docs/getting-started/configuration/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Configuration", + "position": 5, + "link": { + "type": "generated-index", + "description": "See configuration options for the application." + } +} diff --git a/documentation/docs/getting-started/config-options.md b/documentation/docs/getting-started/configuration/config-options.md similarity index 65% rename from documentation/docs/getting-started/config-options.md rename to documentation/docs/getting-started/configuration/config-options.md index 99348eea60..6f272e627a 100644 --- a/documentation/docs/getting-started/config-options.md +++ b/documentation/docs/getting-started/configuration/config-options.md @@ -1,11 +1,16 @@ -# Config Options +--- +sidebar_position: 1 +--- - Changing values in `docker-compose.yml` or in Advanced Settings always **requires a restart** to take effect. Open a terminal, run `docker compose stop` and then - `docker compose up -d` to restart all services. +# Overview Config Options +Changing values in `docker-compose.yml` or in Advanced Settings always **requires a restart** to +take effect. Open a terminal, run `docker compose stop` and then +`docker compose up -d` to restart all services. ## Web Application -There are a few options that can be changed in the web application. + +There are a few options that can be changed in the web application. These options are available though `appsettings.json` and environment variables. @@ -16,13 +21,14 @@ Environment variables are always preferred over `appsettings.json` values. and should prefix with `app__` and replace `:` with `__` and `.` with `_`. So `app__databaseType` is the same as `"app":{"databaseType":"mysql"}` in `appsettings.json`. -- [See advanced configuration options for the web application](../advanced-options/starsky/starsky/readme.md#recommend-settings) +- [See advanced configuration options for the web application](../../advanced-options/starsky/starsky/readme.md#recommend-settings) # Command line options There are separate command line applications that target the specific needs. -See the [Advanced options](../advanced-options/starsky/readme.md) for more information. +See the [Advanced options](../../advanced-options/starsky/readme.md) for more information. Add the command line argument `--help` option to see all available options. -The options are configured in `appsettings.json` and environment variables and command line arguments. +The options are configured in `appsettings.json` and environment variables and command line +arguments. diff --git a/documentation/docs/getting-started/configuration/desktop-open.md b/documentation/docs/getting-started/configuration/desktop-open.md new file mode 100644 index 0000000000..5b271b3814 --- /dev/null +++ b/documentation/docs/getting-started/configuration/desktop-open.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 5 +--- + +# Configure Open With + +There are two options to use the desktop application + +- Remote (So the back-end is not running on your local machine) +- Local / As Desktop + +If you don't know you are using the local one, you can check the settings in the desktop +application. + +When using remote, the application will open the image in the default application. +Then there are no settings to configure overwrites. + +Left: The settings are not available in a remote environment +Right: To check if using remote or local (using the desktop application) + +![Desktop Open](../../assets/getting-started-configuration-desktop-open.jpg) + +## Local / As Desktop + +The following settings are for Local / As Desktop. +The UI writes the settings to the `appsettings.patch.json` file. + +You can use the `appsettings.patch.json` file to configure the desktop application +or use the Settings in the web application. + +### DefaultDesktopEditor + +This setting is done by ImageFormat, +an imageFormat is defined by the first bytes of the file. + +In the UI there is an option to set the default application for a few photo formats. + +> Note: If you enter an invalid ApplicationPath location: The application will open the file with +> the system default application + +```json +{ + "DefaultDesktopEditor": [ + { + "ApplicationPath": "/Applications/Adobe Photoshop 2020/Adobe Photoshop 2020.app", + "ImageFormats": ["jpg", "bmp", "png", "gif", "tiff"] + } + ] +} +``` + +### Collections / Stacks + +When opening an image from a collection/stack, the desktop application will one of both files +A collection is for example two files in one folder: `2021-01-01-IMG_1234.jpg` +and `2021-01-01-IMG_1234.dng` +The default display is to show the jpeg first. + +> Note: The default setting is to open the jpeg file first + +### Raw First + +So the raw file will be open if available + +```json +{ + "DesktopCollectionsOpen": "2" +} +``` + +### Jpeg first + +If the raw file is available, the jpeg file will be open first + +```json +{ + "DesktopCollectionsOpen": "1" +} +``` \ No newline at end of file diff --git a/documentation/docs/getting-started/docker/docker-compose.md b/documentation/docs/getting-started/docker/docker-compose.md index 9e1feab812..d9a2723937 100644 --- a/documentation/docs/getting-started/docker/docker-compose.md +++ b/documentation/docs/getting-started/docker/docker-compose.md @@ -1,48 +1,66 @@ # Setup Using Docker Compose -With [Docker Compose](https://docs.docker.com/compose/), you [use a YAML file](../../developer-guide/technologies/yaml.md) +With [Docker Compose](https://docs.docker.com/compose/), +you [use a YAML file](../../developer-guide/technologies/yaml.md) to configure all application services so you can easily start them with a single command. -Before you proceed, make sure you have [Docker](https://store.docker.com/search?type=edition&offering=community) +Before you proceed, make sure you +have [Docker](https://store.docker.com/search?type=edition&offering=community) installed on your system. It is available for Mac, Linux, and Windows. ## You also could use the application without Docker or Docker compose -Docker is one way of using the application, it also possible to run it without Docker or Docker Compose. + +Docker is one way of using the application, it also possible to run it without Docker or Docker +Compose. ## Step 1 Configure -Download our [docker-compose.yml](https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml) example +Download +our [docker-compose.yml](https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml) +example (right click and *Save Link As...* or use `wget`) to a folder of your choice, -and change the [configuration](../config-options.md) as needed: +and change the [configuration](../configuration/config-options.md) as needed: ```bash wget https://raw.githubusercontent.com/qdraw/starsky/master/starsky/docker/compose/generic/docker-compose.yml ``` Commands on Linux may have to be prefixed with `sudo` when not running as root. -Note that this will point the home directory shortcut `~` to `/root` in the `volumes:` -section of your `docker-compose.yml`. Kernel security modules such as AppArmor and SELinux +Note that this will point the home directory shortcut `~` to `/root` in the `volumes:` +section of your `docker-compose.yml`. Kernel security modules such as AppArmor and SELinux have been [reported to cause issues](../troubleshooting/docker.md#kernel-security). -Ensure that your server has [at least 4 GB of swap](../troubleshooting/docker.md#adding-swap) configured so that +Ensure that your server has [at least 4 GB of swap](../troubleshooting/docker.md#adding-swap) +configured so that indexing doesn't cause restarts when there are memory usage spikes. - #### Database #### -Our example includes a pre-configured [MariaDB](https://mariadb.com/) database server. If you remove it +Our example includes a pre-configured [MariaDB](https://mariadb.com/) database server. If you remove +it and provide no other database server credentials, SQLite database files will be created in the -*storage* folder. Local [SSD storage is best](../troubleshooting/performance.md#storage) for databases of any kind. +*storage* folder. Local [SSD storage is best](../troubleshooting/performance.md#storage) for +databases of any kind. -Never [store database files](../troubleshooting/mariadb.md#corrupted-files) on an unreliable device such as a USB flash drive, SD card, or shared network folder. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files. +Never [store database files](../troubleshooting/mariadb.md#corrupted-files) on an unreliable device +such as a USB flash drive, SD card, or shared network folder. These may also +have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), +which is especially problematic for databases that do not split data into smaller files. > **TL;DR**
-It is not possible to change the password via `MARIADB_PASSWORD` after the database has been started -for the first time. Choosing a secure password is not essential if you don't [expose the database to other apps and hosts](../troubleshooting/mariadb.md#cannot-connect). -To enable [automatic schema updates](../troubleshooting/mariadb.md#auto-upgrade) after upgrading to a new major version, set `MARIADB_AUTO_UPGRADE` to a non-empty value in your `docker-compose.yml`. +> It is not possible to change the password via `MARIADB_PASSWORD` after the database has been +> started +> for the first time. Choosing a secure password is not essential if you +> don't [expose the database to other apps and hosts](../troubleshooting/mariadb.md#cannot-connect). +> To enable [automatic schema updates](../troubleshooting/mariadb.md#auto-upgrade) after upgrading +> to +> a new major version, set `MARIADB_AUTO_UPGRADE` to a non-empty value in your `docker-compose.yml`. #### Volumes #### -Since the app is running inside a container, you have to explicitly [mount the host folders](https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes) you want to use. -The app won't be able to see folders that have not been mounted. That's an important security feature. +Since the app is running inside a container, you have to +explicitly [mount the host folders](https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes) +you want to use. +The app won't be able to see folders that have not been mounted. That's an important security +feature. ##### /app/storageFolder ##### @@ -57,7 +75,8 @@ volumes: - "~/Pictures:/app/storageFolder" ``` -You may [mount any folder accessible from the host](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3) +You +may [mount any folder accessible from the host](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3) instead, including network drives. Additional directories can be mounted as subfolders of `/app/storageFolder`: @@ -76,32 +95,43 @@ volumes: ``` > **TL;DR**
-If *read-only mode* is enabled, all features that require write permission to the *originals/storageFolder* folder -are disabled, uploading and deleting files. Set `app__ReadOnlyFolders__0` to `"/"` -in `docker-compose.yml` for this. -> You can [mount a folder with the `:ro` flag](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3) +> If *read-only mode* is enabled, all features that require write permission to the +*originals/storageFolder* folder +> are disabled, uploading and deleting files. Set `app__ReadOnlyFolders__0` to `"/"` +> in `docker-compose.yml` for this. +> You +> +can [mount a folder with the `:ro` flag](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3) > to make Docker block write operations as well. ##### /app/thumbnailTempFolder ##### Thumbnails files are created in the *thumbnailTempFolder* folder: -- a *storage* folder mount must always be configured in your `docker-compose.yml` file so that you do not lose these files after a restart or upgrade -- never configure the *thumbnailTempFolder* folder to be inside the *thumbnailTempFolder* folder unless the name starts with a `.` to indicate that it is hidden -- we recommend placing the *thumbnailTempFolder* folder on a [local SSD drive](../troubleshooting/performance.md#storage) for best performance -- mounting [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) or using them inside the *thumbnailTempFolder* folder is currently not supported +- a *storage* folder mount must always be configured in your `docker-compose.yml` file so that you + do not lose these files after a restart or upgrade +- never configure the *thumbnailTempFolder* folder to be inside the *thumbnailTempFolder* folder + unless the name starts with a `.` to indicate that it is hidden +- we recommend placing the *thumbnailTempFolder* folder on + a [local SSD drive](../troubleshooting/performance.md#storage) for best performance +- mounting [symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) or using them inside the + *thumbnailTempFolder* folder is currently not supported > **TL;DR**
-Should you later want to move your instance to another host, the easiest and most time-saving way is to copy the entire *storage* folder along with your originals and database. +> Should you later want to move your instance to another host, the easiest and most time-saving way +> is +> to copy the entire *storage* folder along with your originals and database. ##### import ##### -At the moment we don't have a import folder for docker, but you can use the CLI to import files or use web upload +At the moment we don't have a import folder for docker, but you can use the CLI to import files or +use web upload Import in a structured way that avoids duplicates: - imported files receive a canonical filename and will be organized by year and month -- never configure the *import* folder to be inside the *originals* folder, as this will cause a loop by importing already indexed files +- never configure the *import* folder to be inside the *originals* folder, as this will cause a loop + by importing already indexed files ### Step 2: Start the server ### @@ -112,20 +142,25 @@ Run this command to start the application and database services in the backgroun docker compose up -d ``` -*Note that our guides now use the new `docker compose` command by default. If your server does not yet support it, you can still use `docker-compose`.* +*Note that our guides now use the new `docker compose` command by default. If your server does not +yet support it, you can still use `docker-compose`.* Now open the Web UI by navigating to http://localhost:6470/. You should see a registration screen. You may change it on the [account settings page](../../features/accountmanagement.md). -Enabling [public mode](../config-options.md) will disable authentication. +Enabling [public mode](../configuration/config-options.md) will disable authentication. > **Info**
- It can be helpful to [keep Docker running in the foreground while debugging](../troubleshooting/docker.md#viewing-logs) so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting. - Should the server already be running, or you see no errors, you may have started it - on a different host and/or port. There could also be an [issue with your browser, - [ad blocker, or firewall settings](../troubleshooting/index.md). +> It can be helpful +> +to [keep Docker running in the foreground while debugging](../troubleshooting/docker.md#viewing-logs) +> so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting. +> Should the server already be running, or you see no errors, you may have started it +> on a different host and/or port. There could also be an [issue with your browser, +[ad blocker, or firewall settings](../troubleshooting/index.md). -The server port and other [config options](../config-options.md) can be changed in `docker-compose.yml` at any time. +The server port and other [config options](../configuration/config-options.md) can be changed +in `docker-compose.yml` at any time. Remember to restart the services for changes to take effect: ```bash @@ -135,12 +170,16 @@ docker compose up -d ### Step 3: Index Your Library ### -Our [First Steps 👣](../first-steps.md) tutorial guides you through the user interface and settings to ensure your library is indexed according to your individual preferences. +Our [First Steps 👣](../first-steps.md) tutorial guides you through the user interface and settings +to ensure your library is indexed according to your individual preferences. > **Note**
- Ensure [there is enough disk space available](../troubleshooting/docker.md#disk-space) for creating thumbnails and [verify filesystem permissions](../troubleshooting/docker.md) - before starting to index: Files in the *originals* folder must be readable, while the *storage* folder - including all subdirectories must be readable and writeable. +> Ensure [there is enough disk space available](../troubleshooting/docker.md#disk-space) for +> creating +> thumbnails and [verify filesystem permissions](../troubleshooting/docker.md) +> before starting to index: Files in the *originals* folder must be readable, while the *storage* +> folder +> including all subdirectories must be readable and writeable. Open the Web UI, go to *More* and click *Manual Sync* to start indexing your pictures. @@ -148,13 +187,20 @@ Easy, isn't it? ### Troubleshooting ### -If your server runs out of memory, the index is frequently locked, or other system resources are running low: +If your server runs out of memory, the index is frequently locked, or other system resources are +running low: -- [ ] Try [reducing the number of workers](../config-options.md#index-workers) by setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, depending on the CPU performance and number of cores -- [ ] Make sure [your server has at least 4 GB of swap space](../troubleshooting/docker.md#adding-swap) so that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video transcoding are especially demanding +- [ ] Try [reducing the number of workers](../configuration/config-options.md#index-workers) by + setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, + depending on the CPU performance and number of cores +- [ ] Make + sure [your server has at least 4 GB of swap space](../troubleshooting/docker.md#adding-swap) so + that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video + transcoding are especially demanding - [ ] If you are using SQLite, switch to MariaDB, which is better optimized for high concurrency -Other issues? Our [troubleshooting checklists](../troubleshooting/index.md) help you quickly diagnose and solve them. +Other issues? Our [troubleshooting checklists](../troubleshooting/index.md) help you quickly +diagnose and solve them. diff --git a/documentation/docs/getting-started/setup.md b/documentation/docs/getting-started/setup.md index 3027ffd1e6..6c9cb4841f 100644 --- a/documentation/docs/getting-started/setup.md +++ b/documentation/docs/getting-started/setup.md @@ -1,67 +1,105 @@ --- -sidebar_position: 4 +sidebar_position: 5 --- # Setup server app -Starsky can be installed on all operating systems supporting Docker, as well as FreeBSD, Raspberry Pi, and many NAS devices. +Starsky can be installed on all operating systems supporting Docker, as well as FreeBSD, Raspberry +Pi, and many NAS devices. There are multiple ways of installing Starsky: 1. **As background service (systemd or pm2 service)**
Run it as system service. All dependencies are included in the application - There are multiple options to run it as a service, see [systemd](linux-systemd.md), [macOS launchctl](macos-launchctl.md), [windows service](windows-as-server/windows-service.md) or [pm2](pm2.md) for more information + There are multiple options to run it as a service, + see [systemd](linux-systemd.md), [macOS launchctl](macos-launchctl.md), [windows service](windows-as-server/windows-service.md) + or [pm2](pm2.md) for more information 2. **Docker**
- When using Docker we recommend running Starsky with Docker Compose when hosting it on a private server. It is available for Mac, Linux, and Windows. [Read more about docker configuration here](docker/docker-compose.md) + When using Docker we recommend running Starsky with Docker Compose when hosting it on a private + server. It is available for Mac, Linux, and + Windows. [Read more about docker configuration here](docker/docker-compose.md) 3. **In IIS** (Windows Pro and Server Only)
- When running the Pro and Server version of windows the IIS webserver can be used [Read more about IIS configuration here](windows-as-server/iis.md) + When running the Pro and Server version of windows the IIS webserver can be + used [Read more about IIS configuration here](windows-as-server/iis.md) -Once the initial setup is complete, our [First Steps 👣 ](first-steps) tutorial guides you through the user interface and settings to ensure your library is indexed according to your individual preferences. +Once the initial setup is complete, our [First Steps 👣 ](first-steps) tutorial guides you through +the user interface and settings to ensure your library is indexed according to your individual +preferences. -> > Our stable version and development preview have been built into a single multi-arch Docker image for 64-bit AMD, Intel, and ARM processors. That means, Raspberry Pi 3 / 4, Apple Silicon, and other ARM64-based devices can pull from the same repository, enjoy the exact same functionality, and can follow the regular installation instructions after going through a short list of requirements. See FAQs for instructions and notes on alternative installation methods. +> > Our stable version and development preview have been built into a single multi-arch Docker image +> > for 64-bit AMD, Intel, and ARM processors. That means, Raspberry Pi 3 / 4, Apple Silicon, and other +> > ARM64-based devices can pull from the same repository, enjoy the exact same functionality, and can +> > follow the regular installation instructions after going through a short list of requirements. See +> > FAQs for instructions and notes on alternative installation methods. ## Roadmap -Our vision is to provide the most user- and privacy-friendly solution to keep your pictures organized and accessible. The roadmap shows what tasks are in progress, what needs testing, and which features are going to be implemented next. +Our vision is to provide the most user- and privacy-friendly solution to keep your pictures +organized and accessible. The roadmap shows what tasks are in progress, what needs testing, and +which features are going to be implemented next. -We have a low bug policy and do our best to help users when they need support or have other questions. This comes at a price, as we can't give exact deadlines for new features. +We have a low bug policy and do our best to help users when they need support or have other +questions. This comes at a price, as we can't give exact deadlines for new features. -Having said that, funding really has the highest impact. [So users can do their part and become a sponsor to get their favorite features as soon as possible.](https://www.paypal.me/qdrawmedia) +Having said that, funding really has the highest +impact. [So users can do their part and become a sponsor to get their favorite features as soon as possible.](https://www.paypal.me/qdrawmedia) ## System Requirements -You should host Starsky on a server with at least 2 cores, 3 GB of physical memory, 1 and a 64-bit operating system. Beyond these minimum requirements, the amount of RAM should match the number of CPU cores. Indexing large photo and video collections also benefits greatly from local SSD storage, especially for the database and cache files. +You should host Starsky on a server with at least 2 cores, 3 GB of physical memory, 1 and a 64-bit +operating system. Beyond these minimum requirements, the amount of RAM should match the number of +CPU cores. Indexing large photo and video collections also benefits greatly from local SSD storage, +especially for the database and cache files. -If your server has less than 4 GB of swap space or a manual memory/swap limit is set, this can cause unexpected restarts, for example, when the indexer temporarily needs more memory to process large files. High-resolution panoramic images may require additional swap space and/or physical memory above the recommended minimum. +If your server has less than 4 GB of swap space or a manual memory/swap limit is set, this can cause +unexpected restarts, for example, when the indexer temporarily needs more memory to process large +files. High-resolution panoramic images may require additional swap space and/or physical memory +above the recommended minimum. -> We take no responsibility for instability or performance problems if your device does not meet the requirements. +> We take no responsibility for instability or performance problems if your device does not meet the +> requirements. ### Databases -Starsky is compatible with SQLite 3 and MariaDB 10.5.12+.2 Note that SQLite is generally not a good choice for users who require scalability and high performance, and that support for MySQL 8 has been discontinued due to low demand and missing features. +Starsky is compatible with SQLite 3 and MariaDB 10.5.12+.2 Note that SQLite is generally not a good +choice for users who require scalability and high performance, and that support for MySQL 8 has been +discontinued due to low demand and missing features. ### Browsers -Built as a Progressive Web App (PWA), the web interface works with most modern browsers, and runs best on Chrome, Chromium, Safari, Firefox, and Edge. You can conveniently install it on the home screen of all major operating systems and mobile devices. Internet Explorer is not supported. +Built as a Progressive Web App (PWA), the web interface works with most modern browsers, and runs +best on Chrome, Chromium, Safari, Firefox, and Edge. You can conveniently install it on the home +screen of all major operating systems and mobile devices. Internet Explorer is not supported. ### Video playback -Not all video and audio formats can be played with every browser. For example, AAC - the default audio codec for MPEG-4 AVC / H.264 - is supported natively in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and Opera. +Not all video and audio formats can be played with every browser. For example, AAC - the default +audio codec for MPEG-4 AVC / H.264 - is supported natively in Chrome, Safari, and Edge, while it is +only optionally supported by the OS in Firefox and Opera. ### HTTPS -If you install Starsky on a public server outside your home network, always run it behind a secure HTTPS reverse proxy such as Traefik or Caddy. Your files and passwords will otherwise be transmitted in clear text and can be intercepted by anyone, including your provider, hackers, and governments. +If you install Starsky on a public server outside your home network, always run it behind a secure +HTTPS reverse proxy such as Traefik or Caddy. Your files and passwords will otherwise be transmitted +in clear text and can be intercepted by anyone, including your provider, hackers, and governments. ## Getting Support -If you need help installing our software at home, you post your question in GitHub Discussions. Common problems can be quickly diagnosed and solved using our Troubleshooting Checklists. +If you need help installing our software at home, you post your question in GitHub Discussions. +Common problems can be quickly diagnosed and solved using our Troubleshooting Checklists. ### Sponsor us -We'll do our best to answer all your questions. In return, we ask you can sponsor us. Think of "free software" as in "free speech," not as in "free beer". Thank you! +We'll do our best to answer all your questions. In return, we ask you can sponsor us. Think of "free +software" as in "free speech," not as in "free beer". Thank you! -In exchange for their continued support, sponsors are also welcome to request direct technical support via email. Please bear with us if we are unable to get back to you immediately due to the high volume of emails and contact requests we receive. +In exchange for their continued support, sponsors are also welcome to request direct technical +support via email. Please bear with us if we are unable to get back to you immediately due to the +high volume of emails and contact requests we receive. -> > We kindly ask you not to report bugs via GitHub Issues unless you are certain to have found a fully reproducible and previously unreported issue that must be fixed directly in the app. Contact us or a community member if you need help, it could be a local configuration problem, or a misunderstanding in how the software works. +> > We kindly ask you not to report bugs via GitHub Issues unless you are certain to have found a +> > fully reproducible and previously unreported issue that must be fixed directly in the app. Contact +> > us or a community member if you need help, it could be a local configuration problem, or a +> > misunderstanding in how the software works. diff --git a/documentation/docs/getting-started/troubleshooting/index.md b/documentation/docs/getting-started/troubleshooting/index.md index b06593eda3..d55be58a08 100644 --- a/documentation/docs/getting-started/troubleshooting/index.md +++ b/documentation/docs/getting-started/troubleshooting/index.md @@ -1,10 +1,12 @@ # Troubleshooting Checklists -> You are welcome to ask for help in our [discussions](https://github.com/qdraw/starsky/discussions) page +> You are welcome to ask for help in our [discussions](https://github.com/qdraw/starsky/discussions) +> page ### Connection Fails ### -If [your browser](browsers.md) cannot connect to the Web UI even after waiting a few minutes, run this command to display +If [your browser](browsers.md) cannot connect to the Web UI even after waiting a few minutes, run +this command to display the last 100 log messages (omit `--tail=100` to see all): ```bash @@ -13,22 +15,39 @@ docker compose logs --tail=100 Before reporting a bug, whould you please the following things: -- [ ] Check the logs for messages like *disk full*, *disk quota exceeded*, *no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*, *connection failed*, and *killed*: - - [ ] If a service has been "killed" or otherwise automatically terminated, this points to a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage limits) - - [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it) - - [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md) - - [ ] It may help to [add the `:z` mount flag to volumes](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label) when using SELinux (RedHat/Fedora) - - [ ] Log messages that contain "no route to host" indicate a [problem with the database](mariadb.md) or Docker network configuration (follow our [examples](../docker/docker-compose.md)) -- [ ] Make sure you are using the correct protocol (default is `http`), port (default is `4823`), and host (default is `localhost`): - - [ ] Check if the server port you try to use [has been exposed](https://docs.docker.com/compose/compose-file/compose-file-v3/#ports) and [no firewall is blocking it](https://support.microsoft.com/en-us/windows/turn-microsoft-defender-firewall-on-or-off-ec0844f7-aebd-0583-67fe-601ecf5d774f) +- [ ] Check the logs for messages like *disk full*, *disk quota exceeded*, *no space left on + device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*, + *connection failed*, and *killed*: + - [ ] If a service has been "killed" or otherwise automatically terminated, this points to + a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage + limits) + - [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, + either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) + or a disk usage limit is configured (remove or increase it) + - [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" + indicate a [filesystem permission problem](docker.md) + - [ ] It may help + to [add the `:z` mount flag to volumes](https://docs.docker.com/storage/bind-mounts/#configure-the-selinux-label) + when using SELinux (RedHat/Fedora) + - [ ] Log messages that contain "no route to host" indicate + a [problem with the database](mariadb.md) or Docker network configuration (follow + our [examples](../docker/docker-compose.md)) +- [ ] Make sure you are using the correct protocol (default is `http`), port (default is `4823`), + and host (default is `localhost`): + - [ ] Check if the server port you try to + use [has been exposed](https://docs.docker.com/compose/compose-file/compose-file-v3/#ports) + and [no firewall is blocking it](https://support.microsoft.com/en-us/windows/turn-microsoft-defender-firewall-on-or-off-ec0844f7-aebd-0583-67fe-601ecf5d774f) - [ ] Only use `localhost` or `127.0.0.1` if the server is running on the same computer (host) - [ ] Avoid using IP addresses other than `127.0.0.1` directly, as they can change - - [ ] We recommend [configuring a local hostname](../../assets/getting-started-index-pihole-local-dns.png) to access other hosts on your network + - [ ] We + recommend [configuring a local hostname](../../assets/getting-started-index-pihole-local-dns.png) + to access other hosts on your network - [ ] Note that HTTP security headers will prevent the app from loading in a frame (override them) - [ ] Verify your computer meets the [system requirements](../readme.mdx#system-requirements) - [ ] Go through the [checklist for fatal server errors](#fatal-server-errors) -Should MariaDB get stuck in a restart loop and Starsky can't connect to it, this indicates a [memory](docker.md#adding-swap), +Should MariaDB get stuck in a restart loop and Starsky can't connect to it, this indicates +a [memory](docker.md#adding-swap), [filesystem](docker.md), or other [permission issue](docker.md#kernel-security): ``` @@ -38,7 +57,8 @@ starsky: dial tcp 172.18.0.2:3306: connect: no route to host mariadb: mysqld: Shutdown complete ``` -To enable [debug mode](../config-options.md), set `app__verbose` to `true` in the `environment:` section +To enable [debug mode](../configuration/config-options.md), set `app__verbose` to `true` in +the `environment:` section of the `starsky` service (or use the `-v` flag when running the `starsky` command directly): ```yaml @@ -48,8 +68,10 @@ services: app__verbose: "true" ``` -Then restart all services for the changes to take effect. It can be helpful to keep Docker running in the foreground -while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting: +Then restart all services for the changes to take effect. It can be helpful to keep Docker running +in the foreground +while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter +when restarting: ```bash docker compose stop @@ -57,12 +79,16 @@ docker compose up ``` !!! note "" - If you see no errors or no logs at all, you may have started the server on a different host - and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, firewall settings, - or other tools you may have installed. +If you see no errors or no logs at all, you may have started the server on a different host +and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, +firewall settings, +or other tools you may have installed. !!! tldr "" - The default [Docker Compose](https://docs.docker.com/compose/) config filename is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running the `docker-compose` command in the same directory. Config files for other apps or instances should be placed in separate folders. +The default [Docker Compose](https://docs.docker.com/compose/) config filename +is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running +the `docker-compose` command in the same directory. Config files for other apps or instances should +be placed in separate folders. ### Docker Doesn't Work ### @@ -87,44 +113,72 @@ docker compose up Fatal errors are often caused by one of the following conditions: - [ ] Your (virtual) server [disk is full](docker.md#disk-space) (add storage) -- [ ] You have accidentally [mounted the wrong folders](../docker/docker-compose.md#volumes) (update config and restart) -- [ ] There is disk space left, but a usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (change it) -- [ ] You are using a [filesystem or network drive with a file size limit](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/) (change settings or storage) -- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md)) -- [ ] [Symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) were mounted or used within a *storage* folder (replace with actual paths) +- [ ] You have accidentally [mounted the wrong folders](../docker/docker-compose.md#volumes) (update + config and restart) +- [ ] There is disk space left, but a usage or + the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) + has been reached (change it) +- [ ] You are using + a [filesystem or network drive with a file size limit](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/) ( + change settings or storage) +- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) ( + change [permissions](docker.md)) +- [ ] [Symbolic links](https://en.wikipedia.org/wiki/Symbolic_link) were mounted or used within a + *storage* folder (replace with actual paths) - [ ] The [server is low on memory](../readme.mdx#system-requirements) (add memory) - [ ] You didn't [configure at least 4 GB of swap space](docker.md#adding-swap) (add swap) -- [ ] High-resolution panoramic images require [additional memory](performance.md#memory) above the recommended minimum (add more swap or memory) +- [ ] High-resolution panoramic images require [additional memory](performance.md#memory) above the + recommended minimum (add more swap or memory) - [ ] The server CPU is overheating (improve cooling) - [ ] The server has an outdated operating system that is not fully compatible (update) - [ ] The server hardware is defective and causes random panics (test on another server) -- [ ] The [database server](mariadb.md) is not running, [incompatible](../readme.mdx#databases), or misconfigured (start, upgrade, or [fix it](mariadb.md)) -- [ ] You've [upgraded the MariaDB server](mariadb.md#version-upgrade) without running `mariadb-upgrade` -- [ ] Files are [stored on an unreliable device such as a USB flash drive or a shared network folder](mariadb.md#corrupted-files) +- [ ] The [database server](mariadb.md) is not running, [incompatible](../readme.mdx#databases), or + misconfigured (start, upgrade, or [fix it](mariadb.md)) +- [ ] You've [upgraded the MariaDB server](mariadb.md#version-upgrade) without + running `mariadb-upgrade` +- [ ] Files + are [stored on an unreliable device such as a USB flash drive or a shared network folder](mariadb.md#corrupted-files) - [ ] There are network problems caused by a bad configuration, firewall, or unstable connection -- [ ] [Kernel security modules](docker.md#kernel-security) such as [AppArmor](https://wiki.ubuntu.com/AppArmor) and [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) are blocking permissions -- [ ] Your Raspberry Pi has not been configured according to our [recommendations](../raspberry-pi.md#system-requirements) - -We recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, *disk quota exceeded*, -*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no route to host*, *connection failed*, and *killed*: - -- [ ] If a service has been "killed" or otherwise automatically terminated, this points to a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage limits) -- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it) -- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md) -- [ ] Log messages that contain "no route to host" indicate a [problem with the database](mariadb.md) or network configuration (follow our [examples](../docker/docker-compose.md)) - -*Start a full rescan if necessary, for example, if it looks like [thumbnails](index.md#broken-thumbnails) or [pictures are missing](index.md#missing-pictures).* +- [ ] [Kernel security modules](docker.md#kernel-security) such + as [AppArmor](https://wiki.ubuntu.com/AppArmor) + and [SELinux](https://en.wikipedia.org/wiki/Security-Enhanced_Linux) are blocking permissions +- [ ] Your Raspberry Pi has not been configured according to + our [recommendations](../raspberry-pi.md#system-requirements) + +We recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, +*disk quota exceeded*, +*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, *no +route to host*, *connection failed*, and *killed*: + +- [ ] If a service has been "killed" or otherwise automatically terminated, this points to + a [memory problem](docker.md#adding-swap) (add swap and/or memory; remove or increase usage + limits) +- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, + either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a + disk usage limit is configured (remove or increase it) +- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate + a [filesystem permission problem](docker.md) +- [ ] Log messages that contain "no route to host" indicate + a [problem with the database](mariadb.md) or network configuration (follow + our [examples](../docker/docker-compose.md)) + +*Start a full rescan if necessary, for example, if it looks +like [thumbnails](index.md#broken-thumbnails) or [pictures are missing](index.md#missing-pictures).* ### App Not Loading ### -If the app doesn't load in your browser when you navigate to the server URL, you can [check the browser console](browsers.md#getting-error-details) -for helpful errors and warnings. Sometimes you just need to wait a moment, for example, if you are using a slow wireless +If the app doesn't load in your browser when you navigate to the server URL, you +can [check the browser console](browsers.md#getting-error-details) +for helpful errors and warnings. Sometimes you just need to wait a moment, for example, if you are +using a slow wireless connection or the server was started only a few seconds ago. In case this does not help: - [ ] You are using an [incompatible browser](browsers.md) (try another browser) - [ ] JavaScript is disabled in your browser settings, so you only see the splash screen (enable it) - [ ] JavaScript was disabled by a browser plugin (disable it or add an exception) -- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without) +- [ ] Your browser cannot communicate properly with the server, e.g. because + a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its + configuration and try without) - [ ] HTTP security headers prevent the app from loading in a frame (override them) - [ ] An ad blocker or other plugins block requests (disable them or add an exception) - [ ] There is a problem with your network connection (test if other sites work) @@ -132,11 +186,13 @@ connection or the server was started only a few seconds ago. In case this does n ### Missing Pictures ### -If you have indexed your library and some images or videos are missing, first [check *Library > Errors* for errors and warnings](logs.md). +If you have indexed your library and some images or videos are missing, first check the logs In case the application logs don't contain anything helpful: -- [ ] The files exceed the [size limit in megabyte or the resolution limit in megapixels](../config-options.md#storage) -- [ ] The files have [bad filesystem permissions or the wrong owner](docker.md), so they cannot be opened +- [ ] The files exceed + the [size limit in megabyte or the resolution limit in megapixels](../configuration/config-options.md#storage) +- [ ] The files have [bad filesystem permissions or the wrong owner](docker.md), so they cannot be + opened - [ ] The file type is generally unsupported - [ ] The file type is generally supported, but a specific feature or codec is missing - [ ] The indexer has skipped the files because they are exact duplicates @@ -145,87 +201,130 @@ In case the application logs don't contain anything helpful: - [ ] The file is broken, e.g. because of *short Huffman data* (try to fix it) - [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage) - [ ] [The *storage* folder is not writable](docker.md) (change [permissions](docker.md)) - - [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it) + - [ ] A disk usage or + the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) + has been reached (remove or increase it) - [ ] Multiple files were [stacked](../../features/stacks.md) based on their metadata or file names - [ ] You try to index a shared drive on a remote server, but the server is offline -- [ ] The indexer has crashed because you didn't [configure at least 4 GB of swap](docker.md#adding-swap) +- [ ] The indexer has crashed because you + didn't [configure at least 4 GB of swap](docker.md#adding-swap) - [ ] Somebody has deleted files without telling you - [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet -*Depending on the cause of the problem, you may need to perform a full rescan once the issue is resolved.* +*Depending on the cause of the problem, you may need to perform a full rescan once the issue is +resolved.* #### Zip Archives #### -When you try to download multiple pictures and find that some are missing from the resulting zip archive, or you get the error message "No files available for download," your index may be incomplete or out of date (for example, after updating Starsky). A complete rescan of your library may solve the problem. +When you try to download multiple pictures and find that some are missing from the resulting zip +archive, or you get the error message "No files available for download," your index may be +incomplete or out of date (for example, after updating Starsky). A complete rescan of your library +may solve the problem. ### Wrong Search Results ### If search results are incorrect, for example, in the wrong order or not filtered properly: - [ ] Indexing is still in progress and has not been completed yet -- [ ] You need to [re-index your pictures](mariadb.md#complete-rescan), for example after updating Starsky -- [ ] Previously [failed migrations must be re-run](mariadb.md#incompatible-schema) to update the index schema +- [ ] You need to [re-index your pictures](mariadb.md#complete-rescan), for example after updating + Starsky +- [ ] Previously [failed migrations must be re-run](mariadb.md#incompatible-schema) to update the + index schema - [ ] The database server is [incompatible or needs to be updated](../readme.mdx#databases) -*It may be a bug if you cannot find any other reasons, such as a local configuration problem or a misunderstanding in how the software works. Please note that reports must be reproducible in order for us to provide a solution.* +*It may be a bug if you cannot find any other reasons, such as a local configuration problem or a +misunderstanding in how the software works. Please note that reports must be reproducible in order +for us to provide a solution.* ### Broken Thumbnails ### -If some pictures have broken or missing thumbnails, first [check *Library > Errors* for errors and warnings](logs.md). +If some pictures have broken or missing thumbnails, first [check the logs](logs.md). In case the application logs don't contain anything helpful: - [ ] The issue can be resolved by reloading the page or clearing the browser cache - [ ] You browse non-JPEG files in *Library > Originals* which have an icon but no preview - [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage) -- [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it) -- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md)) +- [ ] A disk usage or + the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) + has been reached (remove or increase it) +- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) ( + change [permissions](docker.md)) - [ ] Files were deleted manually, for example to free up disk space - [ ] Files can't be opened, e.g. because the file system permissions have been changed - [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network folder -- [ ] Some thumbnails could not be created because you didn't [configure at least 4 GB of swap](docker.md#adding-swap) -- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without) +- [ ] Some thumbnails could not be created because you + didn't [configure at least 4 GB of swap](docker.md#adding-swap) +- [ ] Your browser cannot communicate properly with the server, e.g. because + a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its + configuration and try without) - [ ] Your proxy, router, or firewall has a request rate limit, so some requests fail - [ ] There are other network problems caused by a firewall, router, or unstable connection - [ ] An ad blocker or other plugins block requests (disable them or add an exception) - [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet -We also recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, *disk quota exceeded*, -*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, and *killed*: +We also recommend checking your [Docker Logs](docker.md#viewing-logs) for messages like *disk full*, +*disk quota exceeded*, +*no space left on device*, *read-only file system*, *error creating path*, *wrong permissions*, and +*killed*: - [ ] If a service has been "killed" or otherwise automatically terminated, this points to a -memory problem -- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a disk usage limit is configured (remove or increase it) -- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate a [filesystem permission problem](docker.md) + memory problem +- [ ] In case the logs show "disk full", "quota exceeded", or "no space left" errors, + either [the disk containing the *storage* folder is full](docker.md#disk-space) (add storage) or a + disk usage limit is configured (remove or increase it) +- [ ] Errors such as "read-only file system", "error creating path", or "wrong permissions" indicate + a [filesystem permission problem](docker.md) -*Depending on the cause of the problem, you may need to perform a full rescan once the issue is resolved.* +*Depending on the cause of the problem, you may need to perform a full rescan once the issue is +resolved.* ### Videos Don't Play ### If videos do not play and/or you only see a white/black area when you open a video: -- [ ] You are using an [incompatible browser](browsers.md), e.g. without AVC support (try another browser) -- [ ] AVC support or related JavaScript features have been disabled in your browser (check the settings and try another browser) +- [ ] You are using an [incompatible browser](browsers.md), e.g. without AVC support (try another + browser) +- [ ] AVC support or related JavaScript features have been disabled in your browser (check the + settings and try another browser) - [ ] An ad blocker or other plugins block requests (disable them or add an exception) - [ ] [Your (virtual) server disk is full](docker.md#disk-space) (add storage) -- [ ] A disk usage or the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) has been reached (remove or increase it) -- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) (change [permissions](docker.md)) -- [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network folder (check if the files are accessible) -- [ ] Your browser cannot communicate properly with the server, e.g. because a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its configuration and try without) -- [ ] There are other network problems caused by a proxy, firewall, or unstable connection (try a direct connection) +- [ ] A disk usage or + the [inode limit](https://serverfault.com/questions/104986/what-is-the-maximum-number-of-files-a-file-system-can-contain) + has been reached (remove or increase it) +- [ ] The *storage* folder [is not writable or mounted read-only](docker.md) ( + change [permissions](docker.md)) +- [ ] Files are stored on an unreliable device such as a USB flash drive or a shared network + folder (check if the files are accessible) +- [ ] Your browser cannot communicate properly with the server, e.g. because + a [Reverse Proxy](../proxies/nginx.md), VPN, or CDN is configured incorrectly (check its + configuration and try without) +- [ ] There are other network problems caused by a proxy, firewall, or unstable connection (try a + direct connection) - [ ] You are connected to the wrong server, VPN, CDN, or a DNS record has not been updated yet -We recommend that you check your [Docker Logs](docker.md#viewing-logs) and [the browser console](browsers.md#getting-error-details) -for messages related to *HTTP requests*, *permissions*, *security*, *FFmpeg*, *videos*, and *file conversion*. +We recommend that you check your [Docker Logs](docker.md#viewing-logs) +and [the browser console](browsers.md#getting-error-details) +for messages related to *HTTP requests*, *permissions*, *security*, *FFmpeg*, *videos*, and *file +conversion*. Please note: -1. Not all [video and audio formats](https://caniuse.com/?search=video%20format) can be [played with every browser](browsers.md). For example, [AAC](https://caniuse.com/aac "Advanced Audio Coding") - the default audio codec for [MPEG-4 AVC / H.264](https://caniuse.com/avc "Advanced Video Coding") - is supported natively in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and Opera. -2. HEVC/H.265 video files can have a `.mp4` file extension too, which is often associated with AVC only. This is because MP4 is a *container* format, meaning that the actual video content may be compressed with H.264, H.265, or something else. The file extension doesn't really tell you anything other than that it's probably a video file. +1. Not all [video and audio formats](https://caniuse.com/?search=video%20format) can + be [played with every browser](browsers.md). For + example, [AAC](https://caniuse.com/aac "Advanced Audio Coding") - the default audio codec + for [MPEG-4 AVC / H.264](https://caniuse.com/avc "Advanced Video Coding") - is supported natively + in Chrome, Safari, and Edge, while it is only optionally supported by the OS in Firefox and + Opera. +2. HEVC/H.265 video files can have a `.mp4` file extension too, which is often associated with AVC + only. This is because MP4 is a *container* format, meaning that the actual video content may be + compressed with H.264, H.265, or something else. The file extension doesn't really tell you + anything other than that it's probably a video file. !!! info "" - **We kindly ask you not to report bugs via *GitHub Issues* unless you are certain to have found a fully reproducible and previously unreported issue that must be fixed directly in the app.** - Ask for technical support if you need help, it could be a local - configuration problem, or a misunderstanding in how the software works. +**We kindly ask you not to report bugs via *GitHub Issues* unless you are certain to have found a +fully reproducible and previously unreported issue that must be fixed directly in the app.** +Ask for technical support if you need help, it could be a local +configuration problem, or a misunderstanding in how the software works. ## Used words @@ -239,7 +338,7 @@ Please note: *[RAW]: image format that contains unprocessed sensor data *[URL]: Web Address *[FFmpeg]: transcodes video files -*[HEVC]: High Efficiency Video Coding / H.265 +*[HEVC]: High Efficiency Video Coding / H.265 *[SQLite]: self-contained, serverless SQL database *[swap]: substitute for physical memory *[host]: Computer, Cloud Server, or VM that runs Starsky diff --git a/documentation/docs/getting-started/troubleshooting/logs.md b/documentation/docs/getting-started/troubleshooting/logs.md index e6437a8732..06768535d7 100644 --- a/documentation/docs/getting-started/troubleshooting/logs.md +++ b/documentation/docs/getting-started/troubleshooting/logs.md @@ -5,34 +5,43 @@ The Electron stores it's cache in these folders: Windows: + ``` C:\Users\\AppData\Roaming\starsky\logs ``` Linux: + ``` ~/.config/starsky/logs ``` OS X: + ``` ~/Library/Application\ Support/starsky/logs ``` ## "Browser" - -If you [have a frontend issue](browsers.md), it is often helpful to check the browser console for errors and warnings. -A console is available in all modern browsers and can be activated via keyboard shortcuts or the browser menu. -Problems with the user interface can be caused by a bug or an [incompatible browser](browsers.md#try-another-browser): -Some [features may not be supported](https://caniuse.com/) by non-standard browsers, as well as nightly, unofficial, +If you [have a frontend issue](browsers.md), it is often helpful to check the browser console for +errors and warnings. +A console is available in all modern browsers and can be activated via keyboard shortcuts or the +browser menu. + +Problems with the user interface can be caused by a bug or +an [incompatible browser](browsers.md#try-another-browser): +Some [features may not be supported](https://caniuse.com/) by non-standard browsers, as well as +nightly, unofficial, or outdated versions. -*In case you don't see any log messages, try reloading the page, as the problem may occur while the page is loading.* +*In case you don't see any log messages, try reloading the page, as the problem may occur while the +page is loading.* **Chrome, Chromium, and Edge** -- press ⌘+Option+J (Mac) or Ctrl+Shift+J (Windows, Linux, Chrome OS) to go directly to the Developer Tools +- press ⌘+Option+J (Mac) or Ctrl+Shift+J (Windows, Linux, Chrome OS) to go directly to the Developer + Tools - or, navigate to *More tools* > *Developer tools* in the browser menu and open the *Console* tab **Firefox** @@ -60,7 +69,8 @@ Run this command to display the last 100 log messages (omit `--tail=100` to see docker compose logs --tail=100 ``` -To enable [debug mode](../config-options.md), set `app__verbose` to `true` in the `environment:` section +To enable [debug mode](../configuration/config-options.md), set `app__verbose` to `true` in +the `environment:` section of the `starsky` service (or use the `-v` flag when running the `starsky` command directly): ```yaml @@ -70,18 +80,24 @@ services: app__verbose: "true" ``` -Then restart all services for the changes to take effect. It can be helpful to keep Docker running in the foreground -while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter when restarting: +Then restart all services for the changes to take effect. It can be helpful to keep Docker running +in the foreground +while debugging so that log messages are displayed directly. To do this, omit the `-d` parameter +when restarting: ```bash docker compose stop docker compose up ``` - -> **Note**
- If you see no errors or no logs at all, you may have started the server on a different host - and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, firewall settings, - or other tools you may have installed. - -> **TL;DR**
- The default [Docker Compose](https://docs.docker.com/compose/) config filename is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running the `docker-compose` command in the same directory. Config files for other apps or instances should be placed in separate folders. + +> **Note**
+> If you see no errors or no logs at all, you may have started the server on a different host +> and/or port. There could also be an [issue with your browser](browsers.md), browser plugins, +> firewall settings, +> or other tools you may have installed. + +> **TL;DR**
+> The default [Docker Compose](https://docs.docker.com/compose/) config filename +> is `docker-compose.yml`. For simplicity, it doesn't need to be specified when running +> the `docker-compose` command in the same directory. Config files for other apps or instances should +> be placed in separate folders. diff --git a/documentation/docs/getting-started/troubleshooting/performance.md b/documentation/docs/getting-started/troubleshooting/performance.md index 2ebda9a1c9..18e77c6ae7 100644 --- a/documentation/docs/getting-started/troubleshooting/performance.md +++ b/documentation/docs/getting-started/troubleshooting/performance.md @@ -2,11 +2,16 @@ ## MariaDB ## -The [InnoDB buffer pool](https://mariadb.com/kb/en/innodb-buffer-pool/) serves as a cache for data and indexes. -It is a key component for optimizing MariaDB performance. Its size should be as large as possible to keep frequently +The [InnoDB buffer pool](https://mariadb.com/kb/en/innodb-buffer-pool/) serves as a cache for data +and indexes. +It is a key component for optimizing MariaDB performance. Its size should be as large as possible to +keep frequently used data in memory and reduce disk I/O - typically the biggest bottleneck. -By default, the buffer pool size is between 128 MB and 512 MB, depending on which configuration example you use. You can change it with the `--innodb-buffer-pool-size` command parameter in the `mariadb:` section of your `docker-compose.yml`. `M` stands for Megabyte, `G` for Gigabyte. Do not use spaces. +By default, the buffer pool size is between 128 MB and 512 MB, depending on which configuration +example you use. You can change it with the `--innodb-buffer-pool-size` command parameter in +the `mariadb:` section of your `docker-compose.yml`. `M` stands for Megabyte, `G` for Gigabyte. Do +not use spaces. If your server has plenty of physical memory, we recommend increasing the size to 1 or 2 GB: @@ -16,17 +21,28 @@ services: command: mysqld --innodb-buffer-pool-size=1G ... ``` -As a rule of thumb, [`Innodb_buffer_pool_pages_free`](https://mariadb.com/kb/en/innodb-status-variables/#innodb_buffer_pool_pages_free) should never be [less than 5% of the total pages](https://vettabase.com/blog/is-innodb-buffer-pool-big-enough/). -You can run the following SQL statement, for example using the [`mariadb` command](https://mariadb.com/kb/en/mysql-command-line-client/) in a terminal, to display the number of free pages and other InnoDB-related status information: +As a rule of +thumb, [`Innodb_buffer_pool_pages_free`](https://mariadb.com/kb/en/innodb-status-variables/#innodb_buffer_pool_pages_free) +should never +be [less than 5% of the total pages](https://vettabase.com/blog/is-innodb-buffer-pool-big-enough/). +You can run the following SQL statement, for example using +the [`mariadb` command](https://mariadb.com/kb/en/mysql-command-line-client/) in a terminal, to +display the number of free pages and other InnoDB-related status information: ```SQL SHOW GLOBAL STATUS LIKE 'Innodb_buffer%'; ``` -Advanced users may adjust additional parameters to further improve performance. Tools such as the [mysqltuner.pl](https://github.com/major/MySQLTuner-perl) script can provide helpful recommendations for this. +Advanced users may adjust additional parameters to further improve performance. Tools such as +the [mysqltuner.pl](https://github.com/major/MySQLTuner-perl) script can provide helpful +recommendations for this. !!! info "Windows and macOS" - If you are using *Docker Desktop* on Windows or macOS, remember to increase the [total memory available](../../assets/getting-started-docker-resources-advanced.jpg) for Docker services. Otherwise, they may run out of resources and cannot benefit from a larger cache size. In case Starsky and MariaDB are running in a virtual machine, its memory size should be increased as well. Restart for changes to take effect. +If you are using *Docker Desktop* on Windows or macOS, remember to increase +the [total memory available](../../assets/getting-started-docker-resources-advanced.jpg) for Docker +services. Otherwise, they may run out of resources and cannot benefit from a larger cache size. In +case Starsky and MariaDB are running in a virtual machine, its memory size should be increased as +well. Restart for changes to take effect. ## Windows ## @@ -34,7 +50,8 @@ Advanced users may adjust additional parameters to further improve performance. ## Storage ## -Local Solid-State Drives (SSDs) are [best for databases](https://mariadb.com/de/resources/blog/how-to-tune-mariadb-write-performance/) +Local Solid-State Drives (SSDs) +are [best for databases](https://mariadb.com/de/resources/blog/how-to-tune-mariadb-write-performance/) of any kind: - database performance extremely benefits from high throughput which HDDs can't provide @@ -42,49 +59,76 @@ of any kind: - due to the HDD seek time, HDDs only support 5% of the reads per second of SSDs - the cost savings from using slow hard disks are minimal -Switching to SSDs makes a big difference, especially for write operations and when the read cache is not +Switching to SSDs makes a big difference, especially for write operations and when the read cache is +not big enough or can't be used. > **note**
- Never store database files on an unreliable device such as a USB flash drive, SD card, or shared network folder. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files. +> Never store database files on an unreliable device such as a USB flash drive, SD card, or shared +> network folder. These may also +> have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), +> which is especially problematic for databases that do not split data into smaller files. ## Memory ## -Indexing large photo and video collections benefits from plenty of memory for [caching](#mariadb) and processing large media files. -Ideally, the amount of RAM should match the number of physical CPU cores. If not, reduce the number of workers +Indexing large photo and video collections benefits from plenty of memory for [caching](#mariadb) +and processing large media files. +Ideally, the amount of RAM should match the number of physical CPU cores. If not, reduce the number +of workers as [explained below](#troubleshooting). - ## Server CPU ## -Last but not least, performance can be limited by your server CPU. If you've tried everything else, then only moving +Last but not least, performance can be limited by your server CPU. If you've tried everything else, +then only moving your instance to a more powerful device or cloud server may help. -Be aware that most [NAS devices](https://kb.synology.com/en-us/DSM/tutorial/What_kind_of_CPU_does_my_NAS_have) are -optimized for minimal power consumption and low production costs. Although their hardware gets faster with each generation, -[benchmarks](https://www.google.com/search?q=cpu+benchmarks) show that even 8-year-old standard desktop CPUs like the [Intel Core i3-4130](https://www.cpubenchmark.net/compare/Intel-Pentium-J3710-vs-Intel-i3-4130/2784vs2015) are often many times faster: +Be aware that +most [NAS devices](https://kb.synology.com/en-us/DSM/tutorial/What_kind_of_CPU_does_my_NAS_have) are +optimized for minimal power consumption and low production costs. Although their hardware gets +faster with each generation, +[benchmarks](https://www.google.com/search?q=cpu+benchmarks) show that even 8-year-old standard +desktop CPUs like +the [Intel Core i3-4130](https://www.cpubenchmark.net/compare/Intel-Pentium-J3710-vs-Intel-i3-4130/2784vs2015) +are often many times faster: ![CPU Benchmark](../../assets/getting-started-troubleshooting-performance-passmark-cpu.svg) ## Legacy Hardware ## -It is a known issue that the user interface and backend operations, especially face recognition, can be slow or even crash on older hardware due to a lack of resources. Like most applications, Starsky has certain requirements and our development process does not include testing on unsupported or unusual hardware. +It is a known issue that the user interface and backend operations, especially face recognition, can +be slow or even crash on older hardware due to a lack of resources. Like most applications, Starsky +has certain requirements and our development process does not include testing on unsupported or +unusual hardware. -In many cases, performance can be improved through optimizations. Since these can prove to be very time-consuming and cost-intensive in practice, users and developers must decide on a case-by-case basis whether this provides sufficient benefit in relation to the costs or whether the use of more powerful hardware is faster and cheaper overall. +In many cases, performance can be improved through optimizations. Since these can prove to be very +time-consuming and cost-intensive in practice, users and developers must decide on a case-by-case +basis whether this provides sufficient benefit in relation to the costs or whether the use of more +powerful hardware is faster and cheaper overall. -We kindly ask you not to open a problem report on GitHub Issues for poor performance on older hardware until a full cause and feasibility analysis has been performed. [GitHub Discussions](https://github.com/qdraw/starsky/discussions) or any of our other public forums and communities are great places to start a discussion. +We kindly ask you not to open a problem report on GitHub Issues for poor performance on older +hardware until a full cause and feasibility analysis has been +performed. [GitHub Discussions](https://github.com/qdraw/starsky/discussions) or any of our other +public forums and communities are great places to start a discussion. -That being said, one of the advantages of open-source software is that users can submit [pull requests](https://github.com/qdraw/starsky) with performance and other enhancements they would like to see implemented. This will result in a much faster solution than waiting for a core team member to remotely analyze your problem and then provide a fix. +That being said, one of the advantages of open-source software is that users can +submit [pull requests](https://github.com/qdraw/starsky) with performance and other enhancements +they would like to see implemented. This will result in a much faster solution than waiting for a +core team member to remotely analyze your problem and then provide a fix. ## Troubleshooting ## -If your server runs out of memory, the index is frequently locked, or other system resources are running low: +If your server runs out of memory, the index is frequently locked, or other system resources are +running low: -- [ ] Try [reducing the number of workers](../config-options.md#index-workers) by setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, depending on the CPU performance and number of cores -- [ ] Make sure [your server has at least 4 GB of swap space](docker.md#adding-swap) so that indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video transcoding are especially demanding +- [ ] Try [reducing the number of workers](../configuration/config-options.md#index-workers) by + setting `app__maxDegreesOfParallelism` to a reasonably small value in `docker-compose.yml`, + depending on the CPU performance and number of cores +- [ ] Make sure [your server has at least 4 GB of swap space](docker.md#adding-swap) so that + indexing doesn't cause restarts when memory usage spikes; RAW image conversion and video + transcoding are especially demanding - [ ] If you are using SQLite, switch to MariaDB, which is better optimized for high concurrency Other issues? Our [troubleshooting checklists](index.md) help you quickly diagnose and solve them. - *[SQLite]: self-contained, serverless SQL database diff --git a/documentation/docs/getting-started/troubleshooting/sqlite.md b/documentation/docs/getting-started/troubleshooting/sqlite.md index 61e432a691..15cef150ad 100644 --- a/documentation/docs/getting-started/troubleshooting/sqlite.md +++ b/documentation/docs/getting-started/troubleshooting/sqlite.md @@ -2,23 +2,40 @@ ## Bad Performance -If you have only a few images, concurrent users, and CPU cores, [SQLite](https://www.sqlite.org/) may seem faster compared to full-fledged database servers like [MariaDB](https://mariadb.com/). +If you have only a few images, concurrent users, and CPU cores, [SQLite](https://www.sqlite.org/) +may seem faster compared to full-fledged database servers like [MariaDB](https://mariadb.com/). -This changes as the index grows and the number of concurrent requests increases. The way MariaDB handles multiple queries is completely different and optimized for high concurrency, while SQLite, for example, locks the index on updates so that other operations have to wait. In the worst case, this can lead to locking errors and timeouts during indexing - especially when combined with a slow disk or network storage. +This changes as the index grows and the number of concurrent requests increases. The way MariaDB +handles multiple queries is completely different and optimized for high concurrency, while SQLite, +for example, locks the index on updates so that other operations have to wait. In the worst case, +this can lead to locking errors and timeouts during indexing - especially when combined with a slow +disk or network storage. -The biggest advantage of SQLite is that you don't need to run a separate database server. This can be very useful for testing and works well if you only have a few thousand files to index. If you are looking for scalability and high performance, it is not a good choice. +The biggest advantage of SQLite is that you don't need to run a separate database server. This can +be very useful for testing and works well if you only have a few thousand files to index. If you are +looking for scalability and high performance, it is not a good choice. [Get MariaDB Performance Tips ›](performance.md#mariadb) ## Locking Errors -If you use [traditional hard drives instead of SSDs](performance.md#storage), you will find that Starsky frequently runs into locking issues with SQLite because your CPU is many times faster than the mechanical heads of your disks. To some extent, this may also happen with solid-state drives, but it is much more likely with slow storage. +If you use [traditional hard drives instead of SSDs](performance.md#storage), you will find that +Starsky frequently runs into locking issues with SQLite because your CPU is many times faster than +the mechanical heads of your disks. To some extent, this may also happen with solid-state drives, +but it is much more likely with slow storage. -You may be able to optimize the behavior and reduce locking errors with SQLite parameters that you can set with the [database config option](../config-options.md#database-connection), but ultimately you should use an SSD if you want to keep SQLite or switch to MariaDB. Please note that our team cannot provide support otherwise. +You may be able to optimize the behavior and reduce locking errors with SQLite parameters that you +can set with the [database config option](../configuration/config-options.md#database-connection), +but ultimately you should use an SSD if you want to keep SQLite or switch to MariaDB. Please note +that our team cannot provide support otherwise. ## Server Crashes -If the server crashes unexpectedly or your database files get corrupted frequently, it is usually because they are stored on an unreliable device such as a USB flash drive, an SD card, or a shared network folder mounted via NFS or CIFS. These may also have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), which is especially problematic for databases that do not split data into smaller files. +If the server crashes unexpectedly or your database files get corrupted frequently, it is usually +because they are stored on an unreliable device such as a USB flash drive, an SD card, or a shared +network folder mounted via NFS or CIFS. These may also +have [unexpected file size limitations](https://thegeekpage.com/fix-the-file-size-exceeds-the-limit-allowed-and-cannot-be-saved/), +which is especially problematic for databases that do not split data into smaller files. - [ ] Never use the same database files with more than one server instance - [ ] Use SSDs instead of traditional hard drives, never use network storage diff --git a/documentation/static/openapi/openapi.json b/documentation/static/openapi/openapi.json index 39a4ca64d6..07e46a6cee 100644 --- a/documentation/static/openapi/openapi.json +++ b/documentation/static/openapi/openapi.json @@ -1967,7 +1967,7 @@ }, { "unresolvedReference": false, - "name": "UseLocalDesktopUi", + "name": "UseLocalDesktop", "in": 0, "required": false, "deprecated": false, @@ -2674,6 +2674,348 @@ "parameters": [], "extensions": {} }, + "/api/desktop-editor/open": { + "operations": { + "Get": { + "tags": [ + { + "name": "DesktopEditor", + "extensions": {}, + "unresolvedReference": false + } + ], + "summary": "Open a file in the default editor or a specific editor on the desktop", + "parameters": [ + { + "unresolvedReference": false, + "name": "f", + "in": 0, + "description": "single or multiple subPaths", + "required": false, + "deprecated": false, + "allowEmptyValue": false, + "explode": false, + "allowReserved": false, + "schema": { + "type": "string", + "default": { + "primitiveType": 4, + "anyType": 0, + "value": "" + }, + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "content": {}, + "extensions": {} + }, + { + "unresolvedReference": false, + "name": "collections", + "in": 0, + "description": "to combine files with the same name before the extension", + "required": false, + "deprecated": false, + "allowEmptyValue": false, + "explode": false, + "allowReserved": false, + "schema": { + "type": "boolean", + "default": { + "primitiveType": 7, + "anyType": 0, + "value": true + }, + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "content": {}, + "extensions": {} + } + ], + "responses": { + "200": { + "description": "returns a list of items from the database", + "headers": {}, + "content": { + "application/json": { + "schema": { + "type": "array", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "items": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "PathImageFormatExistsAppPathModel", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/PathImageFormatExistsAppPathModel", + "referenceV2": "#/definitions/PathImageFormatExistsAppPathModel" + } + }, + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "encoding": {}, + "extensions": {} + } + }, + "links": {}, + "extensions": {}, + "unresolvedReference": false + }, + "204": { + "description": "No Content", + "headers": {}, + "content": { + "application/json": { + "schema": { + "type": "array", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "items": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "PathImageFormatExistsAppPathModel", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/PathImageFormatExistsAppPathModel", + "referenceV2": "#/definitions/PathImageFormatExistsAppPathModel" + } + }, + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "encoding": {}, + "extensions": {} + } + }, + "links": {}, + "extensions": {}, + "unresolvedReference": false + }, + "400": { + "description": "Bad Request", + "headers": {}, + "content": { + "application/json": { + "schema": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "encoding": {}, + "extensions": {} + } + }, + "links": {}, + "extensions": {}, + "unresolvedReference": false + }, + "401": { + "description": "User unauthorized", + "headers": {}, + "content": {}, + "links": {}, + "extensions": {}, + "unresolvedReference": false + }, + "404": { + "description": "subPath not found in the database", + "headers": {}, + "content": {}, + "links": {}, + "extensions": {}, + "unresolvedReference": false + } + }, + "callbacks": {}, + "deprecated": false, + "security": [], + "servers": [], + "extensions": {} + } + }, + "servers": [], + "parameters": [], + "extensions": {} + }, + "/api/desktop-editor/amount-confirmation": { + "operations": { + "Get": { + "tags": [ + { + "name": "DesktopEditor", + "extensions": {}, + "unresolvedReference": false + } + ], + "summary": "Check the amount of files to open before", + "parameters": [ + { + "unresolvedReference": false, + "name": "f", + "in": 0, + "description": "single or multiple subPaths", + "required": false, + "deprecated": false, + "allowEmptyValue": false, + "explode": false, + "allowReserved": false, + "schema": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "content": {}, + "extensions": {} + } + ], + "responses": { + "200": { + "description": "bool, true is no confirmation, false is ask confirmation", + "headers": {}, + "content": { + "application/json": { + "schema": { + "type": "boolean", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "examples": {}, + "encoding": {}, + "extensions": {} + } + }, + "links": {}, + "extensions": {}, + "unresolvedReference": false + }, + "401": { + "description": "User unauthorized", + "headers": {}, + "content": {}, + "links": {}, + "extensions": {}, + "unresolvedReference": false + } + }, + "callbacks": {}, + "deprecated": false, + "security": [], + "servers": [], + "extensions": {} + } + }, + "servers": [], + "parameters": [], + "extensions": {} + }, "/api/disk/mkdir": { "operations": { "Post": { @@ -10397,69 +10739,6 @@ "parameters": [], "extensions": {} }, - "/api/trash/detect-to-use-system-trash": { - "operations": { - "Get": { - "tags": [ - { - "name": "Trash", - "extensions": {}, - "unresolvedReference": false - } - ], - "summary": "Is the system trash supported", - "parameters": [], - "responses": { - "200": { - "description": "the item including the updated content", - "headers": {}, - "content": { - "application/json": { - "schema": { - "type": "boolean", - "readOnly": false, - "writeOnly": false, - "allOf": [], - "oneOf": [], - "anyOf": [], - "required": [], - "properties": {}, - "additionalPropertiesAllowed": true, - "enum": [], - "nullable": false, - "deprecated": false, - "extensions": {}, - "unresolvedReference": false - }, - "examples": {}, - "encoding": {}, - "extensions": {} - } - }, - "links": {}, - "extensions": {}, - "unresolvedReference": false - }, - "401": { - "description": "User unauthorized", - "headers": {}, - "content": {}, - "links": {}, - "extensions": {}, - "unresolvedReference": false - } - }, - "callbacks": {}, - "deprecated": false, - "security": [], - "servers": [], - "extensions": {} - } - }, - "servers": [], - "parameters": [], - "extensions": {} - }, "/api/trash/move-to-trash": { "operations": { "Post": { @@ -10470,7 +10749,7 @@ "unresolvedReference": false } ], - "summary": "(beta) Move a file to the trash", + "summary": "Move a file to the trash", "parameters": [ { "unresolvedReference": false, @@ -11969,7 +12248,94 @@ "extensions": {}, "unresolvedReference": false }, - "syncOnStartup": { + "syncOnStartup": { + "type": "boolean", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "importIgnore": { + "type": "array", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "items": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "videoUseLocalTime": { + "type": "array", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "items": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "CameraMakeModel", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/CameraMakeModel", + "referenceV2": "#/definitions/CameraMakeModel" + } + }, + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "useLocalDesktop": { "type": "boolean", "readOnly": false, "writeOnly": false, @@ -11985,7 +12351,7 @@ "extensions": {}, "unresolvedReference": false }, - "importIgnore": { + "defaultDesktopEditor": { "type": "array", "readOnly": false, "writeOnly": false, @@ -11994,7 +12360,6 @@ "anyOf": [], "required": [], "items": { - "type": "string", "readOnly": false, "writeOnly": false, "allOf": [], @@ -12007,7 +12372,15 @@ "nullable": false, "deprecated": false, "extensions": {}, - "unresolvedReference": false + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "AppSettingsDefaultEditorApplication", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/AppSettingsDefaultEditorApplication", + "referenceV2": "#/definitions/AppSettingsDefaultEditorApplication" + } }, "properties": {}, "additionalPropertiesAllowed": true, @@ -12017,47 +12390,32 @@ "extensions": {}, "unresolvedReference": false }, - "videoUseLocalTime": { - "type": "array", + "desktopCollectionsOpen": { "readOnly": false, "writeOnly": false, "allOf": [], "oneOf": [], "anyOf": [], "required": [], - "items": { - "readOnly": false, - "writeOnly": false, - "allOf": [], - "oneOf": [], - "anyOf": [], - "required": [], - "properties": {}, - "additionalPropertiesAllowed": true, - "enum": [], - "nullable": false, - "deprecated": false, - "extensions": {}, - "unresolvedReference": false, - "reference": { - "type": 0, - "id": "CameraMakeModel", - "isExternal": false, - "isLocal": true, - "referenceV3": "#/components/schemas/CameraMakeModel", - "referenceV2": "#/definitions/CameraMakeModel" - } - }, "properties": {}, "additionalPropertiesAllowed": true, "enum": [], - "nullable": true, + "nullable": false, "deprecated": false, "extensions": {}, - "unresolvedReference": false + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "RawJpegMode", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/RawJpegMode", + "referenceV2": "#/definitions/RawJpegMode" + } }, - "useLocalDesktopUi": { - "type": "boolean", + "desktopEditorAmountBeforeConfirmation": { + "type": "integer", + "format": "int32", "readOnly": false, "writeOnly": false, "allOf": [], @@ -12250,6 +12608,78 @@ "extensions": {}, "unresolvedReference": false }, + "AppSettingsDefaultEditorApplication": { + "type": "object", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": { + "imageFormats": { + "type": "array", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "items": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "ImageFormat", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/ImageFormat", + "referenceV2": "#/definitions/ImageFormat" + } + }, + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "applicationPath": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + } + }, + "additionalPropertiesAllowed": false, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, "AppSettingsKeyValue": { "type": "object", "readOnly": false, @@ -14484,6 +14914,150 @@ "extensions": {}, "unresolvedReference": false }, + "PathImageFormatExistsAppPathModel": { + "type": "object", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": { + "subPath": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "fullFilePath": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "imageFormat": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "ImageFormat", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/ImageFormat", + "referenceV2": "#/definitions/ImageFormat" + } + }, + "status": { + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false, + "reference": { + "type": 0, + "id": "ExifStatus", + "isExternal": false, + "isLocal": true, + "referenceV3": "#/components/schemas/ExifStatus", + "referenceV2": "#/definitions/ExifStatus" + } + }, + "appPath": { + "type": "string", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [], + "nullable": true, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + } + }, + "additionalPropertiesAllowed": false, + "enum": [], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, + "RawJpegMode": { + "type": "integer", + "format": "int32", + "readOnly": false, + "writeOnly": false, + "allOf": [], + "oneOf": [], + "anyOf": [], + "required": [], + "properties": {}, + "additionalPropertiesAllowed": true, + "enum": [ + { + "primitiveType": 0, + "anyType": 0, + "value": 0 + }, + { + "primitiveType": 0, + "anyType": 0, + "value": 1 + }, + { + "primitiveType": 0, + "anyType": 0, + "value": 2 + } + ], + "nullable": false, + "deprecated": false, + "extensions": {}, + "unresolvedReference": false + }, "RelativeObjects": { "type": "object", "readOnly": false, diff --git a/history.md b/history.md index 31ca8cfc3a..b849c7dd75 100644 --- a/history.md +++ b/history.md @@ -42,7 +42,25 @@ Semantic Versioning 2.0.0 is from version 0.1.6+ ## List of versions ## version 0.6.0-beta.2 - _(Unreleased)_ - 2024-02-? {#v0.6.0-beta.2} + - [x] (Changed) Back-end Upgrade to .NET 8 - SDK 8.0.201 (Runtime: 8.0.2) (PR #1402) +- [x] (Added) _Back-end_ Native Open File on Windows & Mac OS (PR #1381) +- [x] (Added) _Back-end_ Native Open File with specific editor on Windows & Mac OS (PR #1381) +- [x] (Added) _Back-end_ AppSettings for Collections / Stacks and Open File (PR #1381) +- [x] (Breaking Change) _Back-end_ Rename UseLocalDesktopUi to UseLocalDesktop (PR #1381) +- [x] (Added) _Back-end_ ImageFormat = ExtensionRolesHelper.ImageFormat.directory (PR #1381) +- [x] (Added) _Back-end_ Add role to info api (PR #1381) +- [x] (Added) _Front-end_ Add settings for Open File (PR #1381) +- [x] (Added) _Back-end_ rename starsky core to starsky.project.web (PR #1381) +- [x] (Removed) _Back-end_ Remove /api/trash/detect-to-use-system-trash (PR #1381) +- [x] (Removed) _Back-end_ Remove verbose option in UI (setting is hidden now) (PR #1381) +- [x] (Added) _Front-end_ German translations (PR #1381) +- [x] (Added) _Front-end_ command + shift + k go to settings now (PR #1381) +- [x] (Removed) _App_ Removed overwrite of open app in desktop (replaced with native open file) + (PR #1381) +- [x] (Added) _App_ Add 'App Settings' to the menu (PR #1381) +- [x] (Added) _Front-end_ Add warning when opening a lot pictures at one: "Do you really want to + edit all of the selected photos?" (PR #1381) ## version 0.6.0-beta.1 - 2024-02-18 {#v0.6.0-beta.1} @@ -193,7 +211,7 @@ _Known issues #1106, #1107 and #1108_ - [x] (Added) _Front-end_ Add MoreMenu remove current folder (PR #1085) - [x] (Changed) _Front-end_ MoreMenu refactor (PR #1085) - [x] (Changed) _Front-end_ Removal of Directories (PR #1085) -- [x] (Changed) _Front-end_ Hide parts of menu in UseLocalDesktopUi mode (PR #1087) +- [x] (Changed) _Front-end_ Hide parts of menu in UseLocalDesktop(Ui) mode (PR #1087) - [x] (Fixed) _Front-end_ Fixed 300 eslint issues (PR #1087) - [x] (Changed) _Back-end_ when deleting in systemTrash mode xmp files are now deleted (PR #1088) - [x] (Changed) _Back-end_ test when deleting in server mode: xmp files are gone fixed (PR #1088) diff --git a/starsky-tools/mock/api/desktop-editor/amount-confirmation.json b/starsky-tools/mock/api/desktop-editor/amount-confirmation.json new file mode 100644 index 0000000000..f32a5804e2 --- /dev/null +++ b/starsky-tools/mock/api/desktop-editor/amount-confirmation.json @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/starsky-tools/mock/api/desktop-editor/open.json b/starsky-tools/mock/api/desktop-editor/open.json new file mode 100644 index 0000000000..61ad49a945 --- /dev/null +++ b/starsky-tools/mock/api/desktop-editor/open.json @@ -0,0 +1,9 @@ +[ + { + "subPath": "/20221029_101722_DSC05623.arw", + "fullFilePath": "/data/testcontent//20221029_101722_DSC05623.arw", + "imageFormat": 12, + "status": 8, + "appPath": "" + } +] diff --git a/starsky-tools/mock/api/env/features.json b/starsky-tools/mock/api/env/features.json index 3b514380a0..ec7f18de73 100644 --- a/starsky-tools/mock/api/env/features.json +++ b/starsky-tools/mock/api/env/features.json @@ -1 +1 @@ -{"systemTrashEnabled":false,"useLocalDesktopUi":false} \ No newline at end of file +{"systemTrashEnabled":false,"useLocalDesktop":false, "openEditorEnabled": true} \ No newline at end of file diff --git a/starsky-tools/mock/set-router.js b/starsky-tools/mock/set-router.js index 1f587ebe1a..3b7450c507 100644 --- a/starsky-tools/mock/set-router.js +++ b/starsky-tools/mock/set-router.js @@ -1,43 +1,45 @@ const express = require("express"); const path = require("path"); -var apiAccountChangeSecretIndex = require("./api/account/change-secret/index.json"); -var apiAccountPermissionsIndex = require("./api/account/permissions/index.json"); +const apiAccountChangeSecretIndex = require("./api/account/change-secret/index.json"); +const apiAccountPermissionsIndex = require("./api/account/permissions/index.json"); -var accountStatus = require("./api/account/status/index.json"); -var apiHealthDetails = require("./api/health/details/index.json"); -var apiHealthCheckForUpdates = require("./api/health/check-for-updates/index.json"); -var apiGeoReverseLookup = require("./api/geo-reverse-lookup/index.json"); +const accountStatus = require("./api/account/status/index.json"); +const apiHealthDetails = require("./api/health/details/index.json"); +const apiHealthCheckForUpdates = require("./api/health/check-for-updates/index.json"); +const apiGeoReverseLookup = require("./api/geo-reverse-lookup/index.json"); -var apiIndexIndex = require("./api/index/index.json"); -var apiIndex__Starsky = require("./api/index/__starsky.json"); -var apiIndex0001 = require("./api/index/0001.json"); -var apiIndex0001_toggleDeleted = require("./api/index/0001_toggleDeleted.json"); +const apiIndexIndex = require("./api/index/index.json"); +const apiIndex__Starsky = require("./api/index/__starsky.json"); +const apiIndex0001 = require("./api/index/0001.json"); +const apiIndex0001_toggleDeleted = require("./api/index/0001_toggleDeleted.json"); -var apiIndex__Starsky01dif = require("./api/index/__starsky_01-dif.json"); -var apiIndex__Starsky01difColorclass0 = require("./api/index/__starsky_01-dif_colorclass0.json"); +const apiIndex__Starsky01dif = require("./api/index/__starsky_01-dif.json"); +const apiIndex__Starsky01difColorclass0 = require("./api/index/__starsky_01-dif_colorclass0.json"); -var apiIndex__Starsky01dif20180101170001 = require("./api/index/__starsky_01-dif-2018.01.01.17.00.01.json"); +const apiIndex__Starsky01dif20180101170001 = require("./api/index/__starsky_01-dif-2018.01.01.17.00.01.json"); -var apiInfo__testJpg = require("./api/info/test.jpg.json"); +const apiInfo__testJpg = require("./api/info/test.jpg.json"); -var apiSearchTrash = require("./api/search/trash/index.json"); -var apiSearch = require("./api/search/index.json"); -var apiSearchTest = require("./api/search/test.json"); -var apiSearchTest1 = require("./api/search/test1.json"); -var apiUpdate__Starsky01dif20180101170001_Deleted = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Deleted.json"); -var apiUpdate__Starsky01dif20180101170001_Ok = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Ok.json"); +const apiSearchTrash = require("./api/search/trash/index.json"); +const apiSearch = require("./api/search/index.json"); +const apiSearchTest = require("./api/search/test.json"); +const apiSearchTest1 = require("./api/search/test1.json"); +const apiUpdate__Starsky01dif20180101170001_Deleted = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Deleted.json"); +const apiUpdate__Starsky01dif20180101170001_Ok = require("./api/update/__starsky_01-dif-2018.01.01.17.00.01_Ok.json"); -var apiEnvIndex = require("./api/env/index.json"); -var apiEnvFeatures = require("./api/env/features.json"); +const apiEnvIndex = require("./api/env/index.json"); +const apiEnvFeatures = require("./api/env/features.json"); -var apiPublishIndex = require("./api/publish/index.json"); -var apiPublishCreateIndex = require("./api/publish/create/index.json"); +const apiPublishIndex = require("./api/publish/index.json"); +const apiPublishCreateIndex = require("./api/publish/create/index.json"); -var githubComReposQdrawStarskyReleaseIndex = require("./github.com/repos/qdraw/starsky/releases/index.json"); +const apiDeskopEditorOpen = require("./api/desktop-editor/open.json"); + +const githubComReposQdrawStarskyReleaseIndex = require("./github.com/repos/qdraw/starsky/releases/index.json"); function setRouter(app, isStoryBook = false) { - var prefix = "/starsky"; + const prefix = "/starsky"; app.use( prefix + "/api/thumbnail", @@ -59,7 +61,7 @@ function setRouter(app, isStoryBook = false) { res.json(accountStatus); }); - var isChangePasswordSuccess = false; + let isChangePasswordSuccess = false; app.post(prefix + "/api/account/change-secret/", (req, res) => { console.log(req.body); @@ -159,7 +161,7 @@ function setRouter(app, isStoryBook = false) { return res.json("not found"); }); - var isDeleted = true; + let isDeleted = true; app.post(prefix + "/api/update", (req, res) => { if (!req.body) { res.statusCode = 500; @@ -248,6 +250,30 @@ function setRouter(app, isStoryBook = false) { return res.json(apiEnvIndex); }); + app.post(prefix + "/api/desktop-editor/amount-confirmation", (req, res) => { + if (!req.body) { + return res.json("no body ~ the normal api does ignore it"); + } + console.log(`amount-confirmation ${req.body.f}`); + + return res.json(req.body.f !== "/true.jpg"); + }); + + app.post(prefix + "/api/desktop-editor/open", (req, res) => { + if (!req.body) { + return res.json("no body ~ the normal api does ignore it"); + } + console.log(`open ${req.body.f}`); + + if (req.body.f === "/true.jpg") { + res.statusCode = 400; + res.json() + return + } + + return res.json(apiDeskopEditorOpen); + }); + app.get(prefix + "/api/health/application-insights", (req, res) => { res.set("Content-Type", "application/javascript"); return res.send(""); @@ -282,7 +308,7 @@ function setRouter(app, isStoryBook = false) { }); // Simulate waiting - var fakeLoading = {}; + let fakeLoading = {}; app.get(prefix + "/export/zip/:id", (req, res) => { if (!fakeLoading[req.params.id]) { fakeLoading[req.params.id] = 0; diff --git a/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs new file mode 100644 index 0000000000..ccc8312200 --- /dev/null +++ b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorDesktopService.cs @@ -0,0 +1,29 @@ +using starsky.feature.desktop.Models; + +namespace starsky.feature.desktop.Interfaces; + +public interface IOpenEditorDesktopService +{ + /// + /// Check if the file is less then the amount of files that are allowed to open + /// If there are more files to open it will return false and the front-end will ask for confirmation + /// + /// dot comma list of paths + /// true is no confirmation and false ask are you sure + bool OpenAmountConfirmationChecker(string f); + + /// + /// Is supported and enabled in the feature toggle + /// + /// Should you use it? + bool IsEnabled(); + + /// + /// Open a file in the default editor or specific editor which is set in the app settings + /// + /// dot comma split list with subPaths + /// should pick raw/jpeg file even its not specified + /// files done and list of results + Task<(bool?, string, List)> OpenAsync(string f, + bool collections); +} diff --git a/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs new file mode 100644 index 0000000000..57f4fa37c4 --- /dev/null +++ b/starsky/starsky.feature.desktop/Interfaces/IOpenEditorPreflight.cs @@ -0,0 +1,9 @@ +using starsky.feature.desktop.Models; + +namespace starsky.feature.desktop.Interfaces; + +public interface IOpenEditorPreflight +{ + Task> PreflightAsync( + List inputFilePaths, bool collections); +} diff --git a/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs b/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs new file mode 100644 index 0000000000..1000784e0a --- /dev/null +++ b/starsky/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModel.cs @@ -0,0 +1,18 @@ +using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; + +namespace starsky.feature.desktop.Models; + +public class PathImageFormatExistsAppPathModel +{ + public string SubPath { get; set; } = string.Empty; + + public string FullFilePath { get; set; } = string.Empty; + + public ExtensionRolesHelper.ImageFormat ImageFormat { get; set; } = + ExtensionRolesHelper.ImageFormat.notfound; + + public FileIndexItem.ExifStatus Status { get; set; } = FileIndexItem.ExifStatus.Default; + + public string AppPath { get; set; } = string.Empty; +} diff --git a/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs b/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs new file mode 100644 index 0000000000..8e38a478a3 --- /dev/null +++ b/starsky/starsky.feature.desktop/Service/OpenEditorDesktopService.cs @@ -0,0 +1,131 @@ +using System.Runtime.CompilerServices; +using starsky.feature.desktop.Interfaces; +using starsky.feature.desktop.Models; +using starsky.foundation.database.Models; +using starsky.foundation.injection; +using starsky.foundation.native.OpenApplicationNative.Interfaces; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Models; + +[assembly: InternalsVisibleTo("starskytest")] + +namespace starsky.feature.desktop.Service; + +[Service(typeof(IOpenEditorDesktopService), InjectionLifetime = InjectionLifetime.Scoped)] +public class OpenEditorDesktopService : IOpenEditorDesktopService +{ + private readonly AppSettings _appSettings; + private readonly IOpenApplicationNativeService _openApplicationNativeService; + private readonly IOpenEditorPreflight _openEditorPreflight; + + public OpenEditorDesktopService(AppSettings appSettings, + IOpenApplicationNativeService openApplicationNativeService, + IOpenEditorPreflight openEditorPreflight) + { + _appSettings = appSettings; + _openApplicationNativeService = openApplicationNativeService; + _openEditorPreflight = openEditorPreflight; + } + + /// + /// Get value from App Settings without getting a negative value + /// + /// setting + private int GetDesktopEditorAmountBeforeConfirmation() + { + var desktopEditorAmountBeforeConfirmation = + _appSettings.DesktopEditorAmountBeforeConfirmation ?? + DesktopEditorAmountBeforeConfirmationDefault; + if ( _appSettings.DesktopEditorAmountBeforeConfirmation <= 1 ) + { + desktopEditorAmountBeforeConfirmation = DesktopEditorAmountBeforeConfirmationDefault; + } + + return desktopEditorAmountBeforeConfirmation; + } + + /// + /// Default Setting for Desktop Editor Amount Before Confirmation + /// + private const int DesktopEditorAmountBeforeConfirmationDefault = 5; + + /// + /// Check for Desktop Editor Amount Before Confirmation + /// + /// dot comma seperated values + /// true + public bool OpenAmountConfirmationChecker(string f) + { + var inputFilePaths = PathHelper.SplitInputFilePaths(f); + return GetDesktopEditorAmountBeforeConfirmation() >= inputFilePaths.Length; + } + + /// + /// Is feature toggle enabled and supported + /// + /// true is feature toggle enabled and supported + public bool IsEnabled() + { + return _appSettings.UseLocalDesktop == true && + _openApplicationNativeService.DetectToUseOpenApplication(); + } + + public async Task<(bool?, string, List)> OpenAsync(string f, + bool collections) + { + var inputFilePaths = PathHelper.SplitInputFilePaths(f); + return await OpenAsync(inputFilePaths.ToList(), collections); + } + + internal async Task<(bool?, string, List)> OpenAsync( + List subPaths, bool collections) + { + if ( _appSettings.UseLocalDesktop == false ) + { + return ( null, "UseLocalDesktop feature toggle is disabled", [] ); + } + + if ( !_openApplicationNativeService.DetectToUseOpenApplication() ) + { + return ( null, "OpenEditor is not supported on this configuration", [] ); + } + + var subPathAndImageFormatList = await _openEditorPreflight + .PreflightAsync(subPaths, collections); + + if ( subPathAndImageFormatList.Count == 0 ) + { + return ( false, "No files selected", [] ); + } + + var (openDefaultList, openWithEditorList) = + FilterListOpenDefaultEditorAndSpecificEditor(subPathAndImageFormatList); + + _openApplicationNativeService.OpenDefault(openDefaultList); + _openApplicationNativeService.OpenApplicationAtUrl(openWithEditorList); + + return ( true, "Opened", subPathAndImageFormatList ); + } + + /// + /// Filter the list + /// First is the list with the files that exists and AppPath is set + /// Second is the list with the files that exists but AppPath is not set + /// + /// + /// + internal static (List, List<(string FullFilePath, string AppPath)>) + FilterListOpenDefaultEditorAndSpecificEditor( + IReadOnlyCollection subPathAndImageFormatList) + { + var appPathList = subPathAndImageFormatList + .Where(p => p.Status == FileIndexItem.ExifStatus.Ok && + string.IsNullOrEmpty(p.AppPath)) + .Select(p => p.FullFilePath).ToList(); + var noAppPathList = subPathAndImageFormatList + .Where(p => p.Status == FileIndexItem.ExifStatus.Ok && + !string.IsNullOrEmpty(p.AppPath)) + .Select(p => ( p.FullFilePath, p.AppPath )).ToList(); + return ( appPathList, noAppPathList ); + } +} diff --git a/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs b/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs new file mode 100644 index 0000000000..94687db487 --- /dev/null +++ b/starsky/starsky.feature.desktop/Service/OpenEditorPreflight.cs @@ -0,0 +1,186 @@ +using starsky.feature.desktop.Interfaces; +using starsky.feature.desktop.Models; +using starsky.foundation.database.Helpers; +using starsky.foundation.database.Interfaces; +using starsky.foundation.database.Models; +using starsky.foundation.injection; +using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Models; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Models; +using starsky.foundation.storage.Storage; + +namespace starsky.feature.desktop.Service; + +[Service(typeof(IOpenEditorPreflight), InjectionLifetime = InjectionLifetime.Scoped)] +public class OpenEditorPreflight : IOpenEditorPreflight +{ + private readonly IQuery _query; + private readonly AppSettings _appSettings; + private readonly IWebLogger _logger; + private readonly IStorage _iStorage; + private readonly IStorage _hostFileSystem; + + public OpenEditorPreflight(IQuery query, AppSettings appSettings, + ISelectorStorage selectorStorage, IWebLogger logger) + { + _query = query; + _appSettings = appSettings; + _logger = logger; + _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _hostFileSystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + } + + public async Task> PreflightAsync( + List inputFilePaths, bool collections) + { + var fileIndexItemList = await GetObjectsToOpenFromDatabase(inputFilePaths, collections); + fileIndexItemList = GroupByFileCollectionName(fileIndexItemList, collections); + + var subPathAndImageFormatList = new List(); + + foreach ( var fileIndexItem in fileIndexItemList ) + { + subPathAndImageFormatList.Add(new PathImageFormatExistsAppPathModel + { + AppPath = GetDesktopEditorPath(fileIndexItem.ImageFormat), + Status = fileIndexItem.Status, + ImageFormat = fileIndexItem.ImageFormat, + SubPath = fileIndexItem.FilePath!, + FullFilePath = _appSettings.DatabasePathToFilePath(fileIndexItem.FilePath!) + }); + } + + return subPathAndImageFormatList; + } + + private string GetDesktopEditorPath(ExtensionRolesHelper.ImageFormat imageFormat) + { + var appSettingsDefaultEditor = _appSettings.DefaultDesktopEditor.Find(p => + p.ImageFormats.Contains(imageFormat)); + + var appPath = appSettingsDefaultEditor?.ApplicationPath ?? string.Empty; + + if ( string.IsNullOrEmpty(appPath) ) + { + return string.Empty; + } + + // Under Mac OS the ApplicationPath is a .app folder + // Under Windows the ApplicationPath is a .exe file + if ( _hostFileSystem.IsFolderOrFile(appPath) != + FolderOrFileModel.FolderOrFileTypeList.Deleted ) + { + return appPath; + } + + _logger.LogError("[OpenEditorPreflight] AppPath not found: " + appPath); + return string.Empty; + } + + internal async Task> GetObjectsToOpenFromDatabase( + List inputFilePaths, bool collections) + { + var resultFileIndexItemsList = await _query.GetObjectsByFilePathAsync( + inputFilePaths, collections); + var fileIndexList = new List(); + + foreach ( var fileIndexItem in resultFileIndexItemsList ) + { + // Files that are not on disk + if ( _iStorage.IsFolderOrFile(fileIndexItem.FilePath!) == + FolderOrFileModel.FolderOrFileTypeList.Deleted ) + { + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + FileIndexItem.ExifStatus.NotFoundSourceMissing, + fileIndexList); + continue; + } + + // Dir is readonly / don't edit + if ( new StatusCodesHelper(_appSettings).IsReadOnlyStatus(fileIndexItem) + == FileIndexItem.ExifStatus.ReadOnly ) + { + StatusCodesHelper.ReturnExifStatusError(fileIndexItem, + FileIndexItem.ExifStatus.ReadOnly, + fileIndexList); + continue; + } + + if ( fileIndexItem.ImageFormat is ExtensionRolesHelper.ImageFormat.xmp + or ExtensionRolesHelper.ImageFormat.meta_json ) + { + continue; + } + + if ( fileIndexItem.Status is FileIndexItem.ExifStatus.Default + or FileIndexItem.ExifStatus.OkAndSame ) + { + fileIndexItem.Status = FileIndexItem.ExifStatus.Ok; + } + + fileIndexList.Add(fileIndexItem); + } + + return fileIndexList.DistinctBy(p => p.FilePath).ToList(); + } + + internal List GroupByFileCollectionName( + IEnumerable fileIndexInputList, bool collections = true) + { + // Skip if no collections, no need to filter on the right file + if ( !collections ) + { + return fileIndexInputList.ToList(); + } + + if ( _appSettings.DesktopCollectionsOpen is CollectionsOpenType.RawJpegMode.Default ) + { + _appSettings.DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Jpeg; + } + + var toOpenResultList = new List(); + + var groupedByName = fileIndexInputList.GroupBy(item => item.FileCollectionName); + foreach ( var group in groupedByName ) + { + if ( group.Count() == 1 ) + { + toOpenResultList.AddRange(group); + continue; + } + + var byOrderResultList = new List(); + + switch ( _appSettings.DesktopCollectionsOpen ) + { + case CollectionsOpenType.RawJpegMode.Jpeg: + byOrderResultList.AddRange(group.Where(p => + p.ImageFormat is ExtensionRolesHelper.ImageFormat.jpg + or ExtensionRolesHelper.ImageFormat.bmp + or ExtensionRolesHelper.ImageFormat.png + or ExtensionRolesHelper.ImageFormat.gif + )); + break; + case CollectionsOpenType.RawJpegMode.Raw: + byOrderResultList.AddRange(group.Where(p => + p.ImageFormat == ExtensionRolesHelper.ImageFormat.tiff)); + break; + } + + // When files are not found in the list, take the first one + if ( byOrderResultList.Count == 0 && group.FirstOrDefault() != null ) + { + byOrderResultList.Add(group.First()); + } + + var fileIndexItem = byOrderResultList.OrderBy(p => p.ImageFormat).First(); + toOpenResultList.Add(fileIndexItem); + } + + // could be that the same file is in multiple collections + return toOpenResultList.DistinctBy(p => p.FilePath).ToList(); + } +} diff --git a/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj b/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj new file mode 100644 index 0000000000..fbabcff611 --- /dev/null +++ b/starsky/starsky.feature.desktop/starsky.feature.desktop.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + + {ca693c59-e50a-4dc6-a2ba-fe7e0caf417a} + 0.6.0-beta.0 + enable + enable + starsky.feature.desktop + + + + + + + + + + + diff --git a/starsky/starsky.feature.import/Services/Import.cs b/starsky/starsky.feature.import/Services/Import.cs index a0104c7b81..8a00bdaf2a 100644 --- a/starsky/starsky.feature.import/Services/Import.cs +++ b/starsky/starsky.feature.import/Services/Import.cs @@ -907,6 +907,7 @@ private async Task CreateNewDatabaseDirectory(string parentPath) { AddToDatabase = DateTime.UtcNow, IsDirectory = true, + ImageFormat = ExtensionRolesHelper.ImageFormat.directory, ColorClass = ColorClassParser.Color.None }; diff --git a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs index 469dcaf294..d4b04a32b0 100644 --- a/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs +++ b/starsky/starsky.feature.trash/Interfaces/IMoveToTrashService.cs @@ -14,13 +14,6 @@ public interface IMoveToTrashService Task> MoveToTrashAsync(List inputFilePaths, bool collections); - /// - /// Is it supported to use the system trash - /// But it does NOT check if the feature toggle is enabled - /// - /// true if supported - bool DetectToUseSystemTrash(); - /// /// Is supported and enabled in the feature toggle /// diff --git a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs index 1f09bfc099..025ce4c603 100644 --- a/starsky/starsky.feature.trash/Services/MoveToTrashService.cs +++ b/starsky/starsky.feature.trash/Services/MoveToTrashService.cs @@ -10,6 +10,7 @@ using starsky.foundation.worker.Interfaces; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.feature.trash.Services; [Service(typeof(IMoveToTrashService), InjectionLifetime = InjectionLifetime.Scoped)] @@ -46,17 +47,7 @@ ITrashConnectionService connectionService public bool IsEnabled() { return _appSettings.UseSystemTrash == true && - _systemTrashService.DetectToUseSystemTrash(); - } - - /// - /// Is it supported to use the system trash - /// But it does NOT check if the feature toggle is enabled - /// - /// true if supported - public bool DetectToUseSystemTrash() - { - return _systemTrashService.DetectToUseSystemTrash(); + _systemTrashService.DetectToUseSystemTrash(); } /// @@ -74,11 +65,13 @@ public async Task> MoveToTrashAsync( await _metaPreflight.PreflightAsync(inputModel, inputFilePaths, false, collections, 0); - (fileIndexResultsList, changedFileIndexItemName) = await AppendChildItemsToTrashList(fileIndexResultsList, changedFileIndexItemName); + ( fileIndexResultsList, changedFileIndexItemName ) = + await AppendChildItemsToTrashList(fileIndexResultsList, changedFileIndexItemName); var moveToTrashList = fileIndexResultsList.Where(p => - p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.Deleted).ToList(); + p.Status is FileIndexItem.ExifStatus.Ok or FileIndexItem.ExifStatus.Deleted) + .ToList(); var isSystemTrashEnabled = IsEnabled(); @@ -92,9 +85,8 @@ await _queue.QueueBackgroundWorkItemAsync(async _ => return; } - await MetaTrashInQueue(changedFileIndexItemName!, + await MetaTrashInQueue(changedFileIndexItemName, fileIndexResultsList, inputModel, collections); - }, "trash"); return TrashConnectionService.StatusUpdate(moveToTrashList, isSystemTrashEnabled); @@ -112,8 +104,9 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName, /// /// /// - internal async Task<(List, Dictionary>?)> AppendChildItemsToTrashList(List moveToTrash, - Dictionary> changedFileIndexItemName) + internal async Task<(List, Dictionary>)> + AppendChildItemsToTrashList(List moveToTrash, + Dictionary> changedFileIndexItemName) { var parentSubPaths = moveToTrash .Where(p => !string.IsNullOrEmpty(p.FilePath) && p.IsDirectory == true) @@ -122,7 +115,7 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName, if ( parentSubPaths.Count == 0 ) { - return (moveToTrash, changedFileIndexItemName); + return ( moveToTrash, changedFileIndexItemName ); } var childItems = ( await _query.GetAllObjectsAsync(parentSubPaths) ) @@ -139,7 +132,7 @@ await _metaUpdateService.UpdateAsync(changedFileIndexItemName, changedFileIndexItemName.TryAdd(childItem.FilePath!, new List { "tags" }); } - return (moveToTrash, changedFileIndexItemName); + return ( moveToTrash, changedFileIndexItemName ); } private async Task SystemTrashInQueue(List moveToTrash) diff --git a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs index 05c63af249..17496fa95d 100644 --- a/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs +++ b/starsky/starsky.foundation.accountmanagement/Interfaces/IUserManager.cs @@ -96,6 +96,8 @@ Task RemoveUser(string credentialTypeCode, Task ExistAsync(int userTableId); Role? GetRole(string credentialTypeCode, string identifier); + + Task GetRoleAsync(int userId); bool PreflightValidate(string userName, string password, string confirmPassword); } } diff --git a/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs b/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs index 73268b6b78..0e16160598 100644 --- a/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs +++ b/starsky/starsky.foundation.accountmanagement/Models/Account/UserStatusModel.cs @@ -10,5 +10,6 @@ public sealed class UserIdentifierStatusModel public DateTime Created { get; set; } public List? CredentialsIdentifiers { get; set; } = new List(); public List? CredentialTypeIds { get; set; } = new List(); + public string? RoleCode { get; set; } } } diff --git a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs index fac0da02e7..2552255262 100644 --- a/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs +++ b/starsky/starsky.foundation.accountmanagement/Services/UserManager.cs @@ -763,6 +763,14 @@ public int GetCurrentUserId(HttpContext httpContext) return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p => p.Id == roleId); } + public async Task GetRoleAsync(int userId) + { + var role = await _dbContext.UserRoles.FirstOrDefaultAsync(p => p.User != null && p.User.Id == userId); + if ( role == null ) return null; + var roleId = role.RoleId; + return _dbContext.Roles.TagWith("GetRole").FirstOrDefault(p => p.Id == roleId); + } + public Credential? GetCredentialsByUserId(int userId) { return _dbContext.Credentials diff --git a/starsky/starsky.foundation.database/Helpers/Duplicate.cs b/starsky/starsky.foundation.database/Helpers/Duplicate.cs index 4811732eda..552e2b1970 100644 --- a/starsky/starsky.foundation.database/Helpers/Duplicate.cs +++ b/starsky/starsky.foundation.database/Helpers/Duplicate.cs @@ -16,11 +16,12 @@ public Duplicate(IQuery query) } /// - /// Check and remove duplicate + /// Check and remove duplicate from database /// /// /// - public async Task> RemoveDuplicateAsync(List databaseSubFolderList) + public async Task> RemoveDuplicateAsync( + List databaseSubFolderList) { // Get a list of duplicate items var duplicateItemsByFilePath = databaseSubFolderList.GroupBy(item => item.FilePath) @@ -37,6 +38,7 @@ public async Task> RemoveDuplicateAsync(List await _query.RemoveItemAsync(duplicateItems[i]); } } + return databaseSubFolderList; } } diff --git a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs index b43520434b..abc6aff159 100644 --- a/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs +++ b/starsky/starsky.foundation.database/Helpers/StatusCodesHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; namespace starsky.foundation.database.Helpers @@ -16,7 +17,8 @@ public StatusCodesHelper(AppSettings appSettings) public FileIndexItem.ExifStatus IsReadOnlyStatus(FileIndexItem fileIndexItem) { - if ( fileIndexItem.IsDirectory == true && _appSettings.IsReadOnly(fileIndexItem.FilePath!) ) + if ( fileIndexItem.IsDirectory == true && + _appSettings.IsReadOnly(fileIndexItem.FilePath!) ) { return FileIndexItem.ExifStatus.DirReadOnly; } @@ -53,13 +55,16 @@ public FileIndexItem.ExifStatus IsReadOnlyStatus(DetailView? detailView) public static FileIndexItem.ExifStatus IsDeletedStatus(FileIndexItem? fileIndexItem) { - return fileIndexItem?.Tags != null && fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ? - FileIndexItem.ExifStatus.Deleted : FileIndexItem.ExifStatus.Default; + return fileIndexItem?.Tags != null && + fileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) + ? FileIndexItem.ExifStatus.Deleted + : FileIndexItem.ExifStatus.Default; } public static FileIndexItem.ExifStatus IsDeletedStatus(DetailView? detailView) { - if ( !string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) && detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) + if ( !string.IsNullOrEmpty(detailView?.FileIndexItem?.Tags) && + detailView.FileIndexItem.Tags.Contains(TrashKeyword.TrashKeywordString) ) { return FileIndexItem.ExifStatus.Deleted; } @@ -83,6 +88,7 @@ public static bool ReturnExifStatusError(FileIndexItem statusModel, { case FileIndexItem.ExifStatus.DirReadOnly: statusModel.IsDirectory = true; + statusModel.ImageFormat = ExtensionRolesHelper.ImageFormat.directory; statusModel.Status = FileIndexItem.ExifStatus.DirReadOnly; fileIndexResultsList.Add(statusModel); return true; @@ -107,6 +113,7 @@ public static bool ReturnExifStatusError(FileIndexItem statusModel, fileIndexResultsList.Add(statusModel); return true; } + return false; } @@ -120,6 +127,7 @@ public static bool ReadonlyDenied(FileIndexItem statusModel, fileIndexResultsList.Add(statusModel); return true; } + return false; } @@ -132,7 +140,5 @@ public static void ReadonlyAllowed(FileIndexItem statusModel, statusModel.Status = FileIndexItem.ExifStatus.ReadOnly; fileIndexResultsList.Add(statusModel); } - - } } diff --git a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs index 964475a218..f4d83ca5d5 100644 --- a/starsky/starsky.foundation.database/Query/QuerySingleItem.cs +++ b/starsky/starsky.foundation.database/Query/QuerySingleItem.cs @@ -119,6 +119,7 @@ public partial class Query if ( currentFileIndexItem.IsDirectory == true ) { currentFileIndexItem.CollectionPaths = new List { singleItemDbPath }; + return new DetailView { IsDirectory = true, diff --git a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs index efccca4dfc..7bbf979784 100644 --- a/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs +++ b/starsky/starsky.foundation.native/Helpers/OperatingSystemHelper.cs @@ -11,20 +11,30 @@ public static OSPlatform GetPlatform() internal delegate bool IsOsPlatformDelegate(OSPlatform osPlatform); + /// + /// Used to make the function testable + /// + /// Delegate to know what the OS is + /// Runtime OS internal static OSPlatform GetPlatformInternal(IsOsPlatformDelegate isOsPlatformDelegate) { if ( isOsPlatformDelegate(OSPlatform.Windows) ) { return OSPlatform.Windows; } + if ( isOsPlatformDelegate(OSPlatform.OSX) ) { return OSPlatform.OSX; } + if ( isOsPlatformDelegate(OSPlatform.Linux) ) { return OSPlatform.Linux; } - return isOsPlatformDelegate(OSPlatform.FreeBSD) ? OSPlatform.FreeBSD : OSPlatform.Create("Unknown"); + + return isOsPlatformDelegate(OSPlatform.FreeBSD) + ? OSPlatform.FreeBSD + : OSPlatform.Create("Unknown"); } } diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs new file mode 100644 index 0000000000..9397fc8b76 --- /dev/null +++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrl.cs @@ -0,0 +1,150 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using starsky.foundation.native.Trash.Helpers; + +namespace starsky.foundation.native.OpenApplicationNative.Helpers; + +[SuppressMessage("Interoperability", + "SYSLIB1054:Use \'LibraryImportAttribute\' instead of \'DllImportAttribute\' to " + + "generate P/Invoke marshalling code at compile time")] +public static class MacOsOpenUrl +{ + /// + /// Add check if not Mac OS X + /// + /// + /// + /// + internal static bool? OpenDefault( + List fileUrls, OSPlatform platform) + { + return platform != OSPlatform.OSX ? null : OpenDefault(fileUrls); + } + + /// + /// Does NOT check if file exists + /// + /// Absolute Path of file + /// + public static bool OpenDefault( + List fileUrls) + { + if ( fileUrls.Count == 0 ) + { + return false; + } + + var fileUrlsIntPtr = MacOsTrashBindingHelper.GetUrls(fileUrls); + + var result = new List(); + foreach ( var fileUrlIntPtr in fileUrlsIntPtr ) + { + result.Add(InvokeOpenUrl(fileUrlIntPtr)); + } + + return result.TrueForAll(p => p); + } + + internal static bool? OpenApplicationAtUrl( + List fileUrls, + string applicationUrl, OSPlatform platform) + { + return platform != OSPlatform.OSX ? null : OpenApplicationAtUrl(fileUrls, applicationUrl); + } + + /// + /// Does NOT check if a file exists + /// No Fallback if NOT Mac OS X + /// + /// Absolute Paths + /// Open with .app folder + /// When not Mac OS + internal static bool? OpenApplicationAtUrl( + List fileUrls, + string applicationUrl) + { + if ( fileUrls.Count == 0 ) + { + return false; + } + + var filesUrlIntPtr = MacOsTrashBindingHelper.GetUrls(fileUrls); + var fileUrlIntPtrUrlArray = MacOsTrashBindingHelper.CreateCfArray(filesUrlIntPtr); + + var applicationUrlIntPtr = + MacOsTrashBindingHelper.GetUrls([applicationUrl]).FirstOrDefault(); + + var nsWorkspaceOpenConfiguration = objc_getClass("NSWorkspaceOpenConfiguration"); + var nsWorkspaceOpenConfigurationDefault = objc_msgSend_retIntPtr( + nsWorkspaceOpenConfiguration, MacOsTrashBindingHelper.GetSelector("configuration")); + + // https://developer.apple.com/documentation/appkit/nsworkspace/3172702-openurls?language=objc + OpenUrLsWithApplicationAtUrl(fileUrlIntPtrUrlArray, applicationUrlIntPtr, + nsWorkspaceOpenConfigurationDefault); + return true; + } + + /// + /// Open Default Url + /// + /// Pointer for urls + /// Is Success + internal static bool InvokeOpenUrl(IntPtr fileUrlIntPtr) + { + return objc_msgSend_retBool_IntPtr_IntPtr( + NsWorkspaceSharedWorkSpace(), + MacOsTrashBindingHelper.GetSelector("openURL:"), + fileUrlIntPtr); + } + + + /// + /// @see: https://developer.apple.com/documentation/appkit/nsworkspace/3172702-openurls?language=objc + /// + internal static void OpenUrLsWithApplicationAtUrl(nint fileUrlIntPtrUrlArray, + nint applicationUrlIntPtr, nint nsWorkspaceOpenConfigurationDefault) + { + objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr_IntPtr( + NsWorkspaceSharedWorkSpace(), + MacOsTrashBindingHelper.GetSelector( + "openURLs:withApplicationAtURL:configuration:completionHandler:"), + fileUrlIntPtrUrlArray, + applicationUrlIntPtr, + nsWorkspaceOpenConfigurationDefault, + IntPtr.Zero); + } + + private const string FoundationFramework = + "/System/Library/Frameworks/Foundation.framework/Foundation"; + + private const string AppKitFramework = + "/System/Library/Frameworks/AppKit.framework/AppKit"; + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern IntPtr objc_msgSend_retIntPtr(IntPtr target, IntPtr selector); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern IntPtr objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr_IntPtr( + IntPtr target, + IntPtr selector, + IntPtr param1, + IntPtr param2, + IntPtr param3, + IntPtr param4); + + [DllImport(AppKitFramework)] + [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments")] + static extern IntPtr objc_getClass(string className); + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern bool objc_msgSend_retBool_IntPtr_IntPtr(IntPtr target, IntPtr selector, + IntPtr param); + + internal static IntPtr NsWorkspaceSharedWorkSpace() + { + // Namespace + var nsWorkspace = objc_getClass("NSWorkspace"); + return objc_msgSend_retIntPtr(nsWorkspace, + MacOsTrashBindingHelper.GetSelector("sharedWorkspace")); + } +} diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs new file mode 100644 index 0000000000..eeb1a099de --- /dev/null +++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopApp.cs @@ -0,0 +1,112 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace starsky.foundation.native.OpenApplicationNative.Helpers; + +public static class WindowsOpenDesktopApp +{ + /// + /// Add check if is Windows + /// + /// full file paths + /// running platform + /// + internal static bool? OpenDefault( + List fileUrls, OSPlatform platform) + { + return platform != OSPlatform.Windows ? null : OpenDefault(fileUrls); + } + + public static bool? OpenDefault(List fileUrls) + { + if ( fileUrls.Count == 0 ) + { + return false; + } + + var result = new List(); + foreach ( var fileUrl in fileUrls ) + { + result.Add(OpenDefault(fileUrl)); + } + + return result.TrueForAll(p => p == true); + } + + /// + /// Does NOT check if file exists + /// + /// Absolute Path of file + /// + public static bool? OpenDefault( + string fileUrl) + { + try + { + var projectStartInfo = new ProcessStartInfo + { + FileName = fileUrl, + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Normal + }; + var projectProcess = Process.Start(projectStartInfo); + return projectProcess != null; + } + catch ( Win32Exception ) + { + return false; + } + } + + /// + /// Skip if is MacOS + /// + /// + /// + /// + /// + internal static bool? OpenApplicationAtUrl( + List fileUrls, + string applicationUrl, OSPlatform platform) + { + return platform != OSPlatform.Windows + ? null + : OpenApplicationAtUrl(fileUrls, applicationUrl); + } + + /// + /// Internal + /// + /// + /// + /// + internal static bool OpenApplicationAtUrl( + List fileUrls, + string applicationUrl) + { + if ( fileUrls.Count == 0 ) + { + return false; + } + + var results = new List(); + foreach ( var url in fileUrls ) + { + var projectStartInfo = new ProcessStartInfo + { + FileName = applicationUrl, + WindowStyle = ProcessWindowStyle.Normal, + Arguments = url + }; + + var process = new Process { StartInfo = projectStartInfo }; + var projectProcess = process.Start(); + results.Add(projectProcess); + + process.Dispose(); + } + + return results.TrueForAll(p => p); + } +} diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs new file mode 100644 index 0000000000..faa4292a0d --- /dev/null +++ b/starsky/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociations.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using starsky.foundation.platform.Helpers; + +namespace starsky.foundation.native.OpenApplicationNative.Helpers; + +public class FileAssociation +{ + public string Extension { get; set; } = string.Empty; + public string ProgId { get; set; } = string.Empty; + public string FileTypeDescription { get; set; } = string.Empty; + public string ExecutableFilePath { get; set; } = string.Empty; +} + +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", + Justification = "Check build in")] +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "InconsistentNaming")] +[SuppressMessage("Performance", "CA1806:Do not ignore method results")] +[SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' " + + "instead of \'DllImportAttribute\' to generate P/Invoke " + + "marshalling code at compile time")] +public static class WindowsSetFileAssociations +{ + /// + /// needed so that Explorer windows get refreshed after the registry is updated + /// https://stackoverflow.com/questions/2681878/associate-file-extension-with-application + /// + /// + /// + /// + /// + /// + [DllImport("Shell32.dll")] + private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2); + + private const int SHCNE_ASSOCCHANGED = 0x8000000; + private const int SHCNF_FLUSH = 0x1000; + + public static bool EnsureAssociationsSet(params FileAssociation[] associations) + { + var madeChanges = false; + foreach ( var association in associations ) + { + madeChanges |= SetAssociation( + association.Extension, + association.ProgId, + association.FileTypeDescription, + association.ExecutableFilePath); + } + + if ( madeChanges ) + { + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero); + } + + return madeChanges; + } + + private static bool SetAssociation(string extension, string progId, string fileTypeDescription, + string applicationFilePath) + { + var madeChanges = false; + madeChanges |= SetKeyValue(@"Software\Classes\" + extension, progId); + madeChanges |= SetKeyValue(@"Software\Classes\" + progId, fileTypeDescription); + madeChanges |= SetKeyValue($@"Software\Classes\{progId}\shell\open\command", + "\"" + applicationFilePath + "\" \"%1\""); + return madeChanges; + } + + internal static bool SetKeyValue(string keyPath, string value) + { + if ( !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ) + { + return false; + } + + return RetryHelper.Do(SetValue, TimeSpan.FromSeconds(1), 2); + + // Can sometimes have: System.IO.IOException: Illegal operation attempted + // on a registry key that has been marked for deletion + bool SetValue() + { + using var key = Registry.CurrentUser.CreateSubKey(keyPath); + if ( key.GetValue(null) as string == value ) return false; + key.SetValue(null, value); + return true; + } + } +} diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs b/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs new file mode 100644 index 0000000000..76c6ea6831 --- /dev/null +++ b/starsky/starsky.foundation.native/OpenApplicationNative/Interfaces/IOpenApplicationNativeService.cs @@ -0,0 +1,27 @@ +namespace starsky.foundation.native.OpenApplicationNative.Interfaces; + +public interface IOpenApplicationNativeService +{ + /// + /// Check if the system is supported to open a file + /// Not all configurations are supported + /// + /// true is supported and false is not supported + bool DetectToUseOpenApplication(); + + /// + /// Open with Default Editor + /// Please check DetectToUseOpenApplication() before using this method + /// + /// List first item is fullFilePath, second is ApplicationUrl + /// open = true, null is unsupported + bool? OpenApplicationAtUrl(List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl); + + /// + /// Open with Default Editor + /// Please check DetectToUseOpenApplication() before using this method + /// + /// Paths on disk + /// open = true, null is unsupported + bool? OpenDefault(List fullPaths); +} diff --git a/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs b/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs new file mode 100644 index 0000000000..0ce2cc0d12 --- /dev/null +++ b/starsky/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeService.cs @@ -0,0 +1,134 @@ +using System.Runtime.InteropServices; +using starsky.foundation.injection; +using starsky.foundation.native.Helpers; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starsky.foundation.native.OpenApplicationNative.Interfaces; + +namespace starsky.foundation.native.OpenApplicationNative; + +[Service(typeof(IOpenApplicationNativeService), InjectionLifetime = InjectionLifetime.Scoped)] +public class OpenApplicationNativeService : IOpenApplicationNativeService +{ + /// + /// Is Open File supported on this configuration + /// + /// true if supported, false if not supported + public bool DetectToUseOpenApplication() + { + return DetectToUseOpenApplicationInternal(RuntimeInformation.IsOSPlatform, + Environment.UserInteractive); + } + + /// + /// Use to overwrite the RuntimeInformation.IsOSPlatform + /// + internal delegate bool IsOsPlatformDelegate(OSPlatform osPlatform); + + /// + /// Is Open File supported on this configuration + /// + /// RuntimeInformation.IsOSPlatform + /// Environment.UserInteractive + /// true if supported, false if not supported + internal static bool DetectToUseOpenApplicationInternal( + IsOsPlatformDelegate runtimeInformationIsOsPlatform, + bool environmentUserInteractive) + { + // Linux is not supported yet + if ( runtimeInformationIsOsPlatform(OSPlatform.Linux) || + runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) ) + { + return false; + } + + // When running in Windows as Service it does not open the application + // On Mac OS it does open the application + if ( !environmentUserInteractive && runtimeInformationIsOsPlatform(OSPlatform.Windows) ) + { + return false; + } + + return true; + } + + + /// + /// Open file with specified application + /// + /// List first item is fullFilePath, second is ApplicationUrl + /// true is operation succeed, false failed | null is platform unsupported + public bool? OpenApplicationAtUrl( + List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl) + { + if ( fullPathAndApplicationUrl.Count == 0 ) + { + return false; + } + + var filesByApplicationPath = SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl); + + var results = new List(); + foreach ( var (fullFilePaths, applicationPath) in filesByApplicationPath ) + { + results.Add(OpenApplicationAtUrl(fullFilePaths, applicationPath)); + } + + if ( results.Contains(null) ) + { + return null; + } + + return results.TrueForAll(p => p == true); + } + + /// + /// Open file with specified application + /// + /// full path style + /// applicationUrl + /// true is operation succeed, false failed | null is platform unsupported + internal static bool? OpenApplicationAtUrl(List fullPaths, string applicationUrl) + { + var currentPlatform = OperatingSystemHelper.GetPlatform(); + var macOsOpenResult = MacOsOpenUrl.OpenApplicationAtUrl(fullPaths, + applicationUrl, currentPlatform); + + var windowsOpenResult = WindowsOpenDesktopApp.OpenApplicationAtUrl(fullPaths, + applicationUrl, currentPlatform); + + return macOsOpenResult ?? windowsOpenResult; + } + + internal static List<(List, string)> SortToOpenFilesByApplicationPath( + List<(string fullFilePath, string applicationUrl)> fullPathAndApplicationUrl) + { + // Group applications by their names + var groupedApplications = fullPathAndApplicationUrl.GroupBy(x => x.Item2).ToList(); + + // Extract full paths for each application and call the implemented function + var results = new List<(List, string)>(); + foreach ( var group in groupedApplications ) + { + var fullPaths = group.Select(item => item.Item1).ToList(); + var applicationUrl = group.Key; + results.Add(( fullPaths, applicationUrl )); + } + + return results; + } + + /// + /// Open file with default application + /// + /// full path style + /// true is operation succeed, false failed | null is platform unsupported + public bool? OpenDefault(List fullPaths) + { + var currentPlatform = OperatingSystemHelper.GetPlatform(); + var macOsOpenResult = MacOsOpenUrl.OpenDefault(fullPaths, currentPlatform); + var windowsOpenResult = WindowsOpenDesktopApp.OpenDefault(fullPaths, + currentPlatform); + + return macOsOpenResult ?? windowsOpenResult; + } +} diff --git a/starsky/starsky.foundation.native/References/OpenDefaultApp.bak b/starsky/starsky.foundation.native/References/OpenDefaultApp.bak new file mode 100644 index 0000000000..be0727ac0f --- /dev/null +++ b/starsky/starsky.foundation.native/References/OpenDefaultApp.bak @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using starsky.foundation.native.Trash.Helpers; + +namespace starsky.foundation.native.OpenApplicationNative.Helpers; + +using System; +using System.Runtime.InteropServices; + +public class MacOsOpenDefaultApp +{ + public static void SetDefaultApplicationAtURL( + string applicationURL, + string fileURL) + { + var nsUrl = MacOsTrashBindingHelper.GetUrls([applicationURL]).FirstOrDefault(); + var fileUrl = MacOsTrashBindingHelper.GetUrls([fileURL]).FirstOrDefault(); + + objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr( + MacOsOpenUrl.NsWorkspaceSharedWorksPace(), + MacOsTrashBindingHelper.GetSelector( + "setDefaultApplicationAtURL:toOpenFileAtURL:completionHandler:"), + nsUrl, + fileUrl, + IntPtr.Zero); + } + + private const string FoundationFramework = + "/System/Library/Frameworks/Foundation.framework/Foundation"; + + [DllImport(FoundationFramework, EntryPoint = "objc_msgSend")] + private static extern void objc_msgSend_retVoid_IntPtr_IntPtr_IntPtr( + IntPtr target, + IntPtr selector, + IntPtr param1, + IntPtr param2, + IntPtr param3); + + + +} diff --git a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs index 93e54f568c..5a6ed0957c 100644 --- a/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs +++ b/starsky/starsky.foundation.native/Trash/Helpers/MacOSTrashBindingHelper.cs @@ -79,6 +79,11 @@ internal static void TrashInternal(List filesFullPath) // CFRelease the fileUrl, sharedWorkspace, nsWorkspace gives a crash (error 139) } + /// + /// Get Selector in the Objective-C runtime + /// + /// Name + /// Object internal static IntPtr GetSelector(string name) { var cfStrSelector = CreateCfString(name); diff --git a/starsky/starsky.foundation.native/Trash/TrashService.cs b/starsky/starsky.foundation.native/Trash/TrashService.cs index 55a3734776..cc5b79d1ff 100644 --- a/starsky/starsky.foundation.native/Trash/TrashService.cs +++ b/starsky/starsky.foundation.native/Trash/TrashService.cs @@ -9,7 +9,6 @@ namespace starsky.foundation.native.Trash; [Service(typeof(ITrashService), InjectionLifetime = InjectionLifetime.Scoped)] public class TrashService : ITrashService { - /// /// Is the system trash supported /// @@ -33,14 +32,15 @@ public bool DetectToUseSystemTrash() /// Environment.UserInteractive /// Environment.UserName /// true if supported, false if not supported - internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtimeInformationIsOsPlatform, + internal static bool DetectToUseSystemTrashInternal( + IsOsPlatformDelegate runtimeInformationIsOsPlatform, bool environmentUserInteractive, string environmentUserName) { // ReSharper disable once ConvertIfStatementToReturnStatement if ( runtimeInformationIsOsPlatform(OSPlatform.Linux) || - runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) || - environmentUserName == "root" || !environmentUserInteractive ) + runtimeInformationIsOsPlatform(OSPlatform.FreeBSD) || + environmentUserName == "root" || !environmentUserInteractive ) { return false; } @@ -63,10 +63,7 @@ internal static bool DetectToUseSystemTrashInternal(IsOsPlatformDelegate runtime /// operation succeed (NOT if file is gone) public bool? Trash(string fullPath) { - var currentPlatform = OperatingSystemHelper.GetPlatform(); - var macOsTrash = MacOsTrashBindingHelper.Trash(fullPath, currentPlatform); - var (windowsTrash, _) = WindowsShellTrashBindingHelper.Trash(fullPath, currentPlatform); - return macOsTrash ?? windowsTrash; + return Trash([fullPath]); } public bool? Trash(List fullPaths) diff --git a/starsky/starsky.foundation.native/starsky.foundation.native.csproj b/starsky/starsky.foundation.native/starsky.foundation.native.csproj index 7918c8d1ee..3d1dd59be7 100644 --- a/starsky/starsky.foundation.native/starsky.foundation.native.csproj +++ b/starsky/starsky.foundation.native/starsky.foundation.native.csproj @@ -12,7 +12,7 @@ - + + - diff --git a/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs b/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs new file mode 100644 index 0000000000..5f54929d1b --- /dev/null +++ b/starsky/starsky.foundation.platform/Enums/CollectionsOpenType.cs @@ -0,0 +1,11 @@ +namespace starsky.foundation.platform.Enums; + +public static class CollectionsOpenType +{ + public enum RawJpegMode + { + Default = 0, + Jpeg = 1, + Raw = 2, + } +} diff --git a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs index dcf80bef8c..60b1395490 100644 --- a/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/AppSettingsCompareHelper.cs @@ -3,6 +3,8 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; +using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Helpers.Compare; using starsky.foundation.platform.JsonConverter; using starsky.foundation.platform.Models; @@ -10,7 +12,6 @@ namespace starsky.foundation.platform.Helpers { public static class AppSettingsCompareHelper { - /// /// Compare a fileIndex item and update items if there are changed in the updateObject /// append => (propertyName == "Tags" add it with comma space or with single space) @@ -21,8 +22,10 @@ public static class AppSettingsCompareHelper public static List Compare(AppSettings sourceIndexItem, object? updateObject = null) { updateObject ??= new AppSettings(); - var propertiesA = sourceIndexItem.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); - var propertiesB = updateObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propertiesA = sourceIndexItem.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propertiesB = updateObject.GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance); var differenceList = new List(); foreach ( var propertyB in propertiesB ) @@ -31,42 +34,91 @@ public static List Compare(AppSettings sourceIndexItem, object? updateOb var propertyInfoFromA = Array.Find(propertiesA, p => p.Name == propertyB.Name); if ( propertyInfoFromA == null ) continue; - CompareMultipleSingleItems(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); - CompareMultipleListDictionary(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); - CompareMultipleObjects(propertyB, propertyInfoFromA, sourceIndexItem, updateObject, differenceList); - + CompareMultipleSingleItems(propertyB, propertyInfoFromA, sourceIndexItem, + updateObject, differenceList); + CompareMultipleListDictionary(propertyB, propertyInfoFromA, sourceIndexItem, + updateObject, differenceList); + CompareListMultipleObjects(propertyB, propertyInfoFromA, sourceIndexItem, + updateObject, + differenceList); } + return differenceList; } - private static void CompareMultipleObjects(PropertyInfo propertyB, PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject, List differenceList) + private static void CompareListMultipleObjects(PropertyInfo propertyB, + PropertyInfo propertyInfoFromA, AppSettings sourceIndexItem, object updateObject, + List differenceList) { - if ( propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) && propertyB.PropertyType == typeof(OpenTelemetrySettings) ) + if ( propertyInfoFromA.PropertyType == typeof(OpenTelemetrySettings) && + propertyB.PropertyType == typeof(OpenTelemetrySettings) ) + { + var oldObjectValue = + ( OpenTelemetrySettings? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newObjectValue = + ( OpenTelemetrySettings? )propertyB.GetValue(updateObject, null); + CompareOpenTelemetrySettingsObject(propertyB.Name, sourceIndexItem, oldObjectValue, + newObjectValue, differenceList); + } + + if ( propertyInfoFromA.PropertyType == + typeof(List) && + propertyB.PropertyType == typeof(List) ) { - var oldObjectValue = ( OpenTelemetrySettings? )propertyInfoFromA.GetValue(sourceIndexItem, null); - var newObjectValue = ( OpenTelemetrySettings? )propertyB.GetValue(updateObject, null); - CompareOpenTelemetrySettingsObject(propertyB.Name, sourceIndexItem, oldObjectValue, newObjectValue, differenceList); + var oldObjectValue = + ( List? )propertyInfoFromA.GetValue( + sourceIndexItem, null); + var newObjectValue = + ( List? )propertyB.GetValue(updateObject, + null); + CompareAppSettingsDefaultEditorApplication(propertyB.Name, sourceIndexItem, + oldObjectValue, + newObjectValue, differenceList); } } - [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] - private static void CompareOpenTelemetrySettingsObject(string propertyName, AppSettings? sourceIndexItem, + [SuppressMessage("Performance", + "CA1859:Use concrete types when possible for improved performance")] + private static void CompareOpenTelemetrySettingsObject(string propertyName, + AppSettings? sourceIndexItem, OpenTelemetrySettings? oldKeyValuePairStringStringValue, - OpenTelemetrySettings? newKeyValuePairStringStringValue, ICollection differenceList) + OpenTelemetrySettings? newKeyValuePairStringStringValue, + ICollection differenceList) { if ( oldKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue == null || - // compare lists - JsonSerializer.Serialize(oldKeyValuePairStringStringValue) == - JsonSerializer.Serialize(newKeyValuePairStringStringValue) || - // default options - JsonSerializer.Serialize(newKeyValuePairStringStringValue) == - JsonSerializer.Serialize(new OpenTelemetrySettings()) ) + newKeyValuePairStringStringValue == null || + // compare lists + JsonSerializer.Serialize(oldKeyValuePairStringStringValue) == + JsonSerializer.Serialize(newKeyValuePairStringStringValue) || + // default options + JsonSerializer.Serialize(newKeyValuePairStringStringValue) == + JsonSerializer.Serialize(new OpenTelemetrySettings()) ) { return; } - sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newKeyValuePairStringStringValue, null); + sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, + newKeyValuePairStringStringValue, null); + differenceList.Add(propertyName.ToLowerInvariant()); + } + + private static void CompareAppSettingsDefaultEditorApplication(string propertyName, + AppSettings? sourceIndexItem, + List? oldKeyValuePairStringStringValue, + List? newKeyValuePairStringStringValue, + List differenceList) + { + if ( oldKeyValuePairStringStringValue == null || + newKeyValuePairStringStringValue == null || + newKeyValuePairStringStringValue.Count == 0 || + AreListsEqualHelper.AreListsEqual(oldKeyValuePairStringStringValue, + newKeyValuePairStringStringValue) ) + { + return; + } + + sourceIndexItem?.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, + newKeyValuePairStringStringValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -75,34 +127,53 @@ private static void CompareMultipleSingleItems(PropertyInfo propertyB, AppSettings sourceIndexItem, object updateObject, List differenceList) { - if ( propertyInfoFromA.PropertyType == typeof(bool?) && propertyB.PropertyType == typeof(bool?) ) + if ( propertyInfoFromA.PropertyType == typeof(bool?) && + propertyB.PropertyType == typeof(bool?) ) { var oldBoolValue = ( bool? )propertyInfoFromA.GetValue(sourceIndexItem, null); var newBoolValue = ( bool? )propertyB.GetValue(updateObject, null); - CompareBool(propertyB.Name, sourceIndexItem, oldBoolValue, newBoolValue, differenceList); + CompareBool(propertyB.Name, sourceIndexItem, oldBoolValue, newBoolValue, + differenceList); } if ( propertyB.PropertyType == typeof(string) ) { var oldStringValue = ( string? )propertyInfoFromA.GetValue(sourceIndexItem, null); var newStringValue = ( string? )propertyB.GetValue(updateObject, null); - CompareString(propertyB.Name, sourceIndexItem, oldStringValue!, newStringValue!, differenceList); + CompareString(propertyB.Name, sourceIndexItem, oldStringValue!, newStringValue!, + differenceList); } if ( propertyB.PropertyType == typeof(int) ) { var oldIntValue = ( int )propertyInfoFromA.GetValue(sourceIndexItem, null)!; var newIntValue = ( int )propertyB.GetValue(updateObject, null)!; - CompareInt(propertyB.Name, sourceIndexItem, oldIntValue, newIntValue, differenceList); + CompareInt(propertyB.Name, sourceIndexItem, oldIntValue, newIntValue, + differenceList); } if ( propertyB.PropertyType == typeof(AppSettings.DatabaseTypeList) ) { - var oldListStringValue = ( AppSettings.DatabaseTypeList? )propertyInfoFromA.GetValue(sourceIndexItem, null); - var newListStringValue = ( AppSettings.DatabaseTypeList? )propertyB.GetValue(updateObject, null); + var oldListStringValue = + ( AppSettings.DatabaseTypeList? )propertyInfoFromA.GetValue(sourceIndexItem, + null); + var newListStringValue = + ( AppSettings.DatabaseTypeList? )propertyB.GetValue(updateObject, null); CompareDatabaseTypeList(propertyB.Name, sourceIndexItem, oldListStringValue, newListStringValue, differenceList); } + + if ( propertyB.PropertyType == typeof(CollectionsOpenType.RawJpegMode) ) + { + var oldRawJpegModeEnumItem = + ( CollectionsOpenType.RawJpegMode? )propertyInfoFromA.GetValue(sourceIndexItem, + null); + var newRawJpegModeEnumItem = + ( CollectionsOpenType.RawJpegMode? )propertyB.GetValue(updateObject, null); + CompareCollectionsOpenTypeRawJpegMode(propertyB.Name, sourceIndexItem, + oldRawJpegModeEnumItem, + newRawJpegModeEnumItem, differenceList); + } } private static void CompareMultipleListDictionary(PropertyInfo propertyB, @@ -111,7 +182,8 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB, { if ( propertyB.PropertyType == typeof(List) ) { - var oldListStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var oldListStringValue = + ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); var newListStringValue = ( List? )propertyB.GetValue(updateObject, null); CompareListString(propertyB.Name, sourceIndexItem, oldListStringValue, newListStringValue, differenceList); @@ -119,19 +191,26 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB, if ( propertyB.PropertyType == typeof(List) ) { - var oldKeyValuePairStringStringValue = ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); - var newKeyValuePairStringStringValue = ( List? )propertyB.GetValue(updateObject, null); - CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem, oldKeyValuePairStringStringValue!, + var oldKeyValuePairStringStringValue = + ( List? )propertyInfoFromA.GetValue(sourceIndexItem, null); + var newKeyValuePairStringStringValue = + ( List? )propertyB.GetValue(updateObject, null); + CompareKeyValuePairStringString(propertyB.Name, sourceIndexItem, + oldKeyValuePairStringStringValue!, newKeyValuePairStringStringValue!, differenceList); } - if ( propertyB.PropertyType == typeof(Dictionary>) ) + if ( propertyB.PropertyType == + typeof(Dictionary>) ) { - var oldListPublishProfilesValue = ( Dictionary>? ) + var oldListPublishProfilesValue = + ( Dictionary>? ) propertyInfoFromA.GetValue(sourceIndexItem, null); - var newListPublishProfilesValue = ( Dictionary>? ) + var newListPublishProfilesValue = + ( Dictionary>? ) propertyB.GetValue(updateObject, null); - CompareListPublishProfiles(propertyB.Name, sourceIndexItem, oldListPublishProfilesValue, + CompareListPublishProfiles(propertyB.Name, sourceIndexItem, + oldListPublishProfilesValue, newListPublishProfilesValue, differenceList); } @@ -146,40 +225,45 @@ private static void CompareMultipleListDictionary(PropertyInfo propertyB, } } - private static void CompareStringDictionary(string propertyName, AppSettings sourceIndexItem, + private static void CompareStringDictionary(string propertyName, + AppSettings sourceIndexItem, Dictionary? oldDictionaryValue, Dictionary? newDictionaryValue, List differenceList) { if ( oldDictionaryValue == null || newDictionaryValue?.Count == 0 ) return; if ( JsonSerializer.Serialize(oldDictionaryValue, - DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue, - DefaultJsonSerializer.CamelCase) ) + DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newDictionaryValue, + DefaultJsonSerializer.CamelCase) ) { return; } - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDictionaryValue, null); + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newDictionaryValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } - private static void CompareKeyValuePairStringString(string propertyName, AppSettings sourceIndexItem, + private static void CompareKeyValuePairStringString(string propertyName, + AppSettings sourceIndexItem, List? oldKeyValuePairStringStringValue, - List? newKeyValuePairStringStringValue, List differenceList) + List? newKeyValuePairStringStringValue, + List differenceList) { if ( oldKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue == null || - newKeyValuePairStringStringValue.Count == 0 ) + newKeyValuePairStringStringValue == null || + newKeyValuePairStringStringValue.Count == 0 ) { return; } if ( oldKeyValuePairStringStringValue.Equals( - newKeyValuePairStringStringValue) ) + newKeyValuePairStringStringValue) ) { return; } - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newKeyValuePairStringStringValue, null); + sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, + newKeyValuePairStringStringValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -191,12 +275,35 @@ private static void CompareKeyValuePairStringString(string propertyName, AppSett /// oldDatabaseTypeList to compare with newDatabaseTypeList /// newDatabaseTypeList to compare with oldDatabaseTypeList /// list of different values - internal static void CompareDatabaseTypeList(string propertyName, AppSettings sourceIndexItem, + internal static void CompareDatabaseTypeList(string propertyName, + AppSettings sourceIndexItem, AppSettings.DatabaseTypeList? oldDatabaseTypeList, AppSettings.DatabaseTypeList? newDatabaseTypeList, List differenceList) { - if ( oldDatabaseTypeList == newDatabaseTypeList || newDatabaseTypeList == new AppSettings().DatabaseType ) return; - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newDatabaseTypeList, null); + if ( oldDatabaseTypeList == newDatabaseTypeList || + newDatabaseTypeList == new AppSettings().DatabaseType ) return; + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newDatabaseTypeList, null); + differenceList.Add(propertyName.ToLowerInvariant()); + } + + /// + /// Compare DatabaseTypeList type + /// + /// name of property e.g. DatabaseTypeList + /// source object + /// oldDatabaseTypeList to compare with newDatabaseTypeList + /// newDatabaseTypeList to compare with oldDatabaseTypeList + /// list of different values + private static void CompareCollectionsOpenTypeRawJpegMode(string propertyName, + AppSettings sourceIndexItem, + CollectionsOpenType.RawJpegMode? oldDatabaseTypeList, + CollectionsOpenType.RawJpegMode? newDatabaseTypeList, List differenceList) + { + if ( oldDatabaseTypeList == newDatabaseTypeList || + newDatabaseTypeList == CollectionsOpenType.RawJpegMode.Default ) return; + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newDatabaseTypeList, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -210,17 +317,19 @@ internal static void CompareDatabaseTypeList(string propertyName, AppSettings so /// newListStringValue to compare with oldListStringValue /// list of different values internal static void CompareListString(string propertyName, AppSettings sourceIndexItem, - List? oldListStringValue, List? newListStringValue, List differenceList) + List? oldListStringValue, List? newListStringValue, + List differenceList) { if ( oldListStringValue == null || newListStringValue?.Count == 0 ) return; if ( JsonSerializer.Serialize(oldListStringValue, - DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue, - DefaultJsonSerializer.CamelCase) ) + DefaultJsonSerializer.CamelCase) == JsonSerializer.Serialize(newListStringValue, + DefaultJsonSerializer.CamelCase) ) { return; } - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListStringValue, null); + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newListStringValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -232,14 +341,17 @@ internal static void CompareListString(string propertyName, AppSettings sourceIn /// oldListPublishValue to compare with newListPublishValue /// newListPublishValue to compare with oldListPublishValue /// list of different values - internal static void CompareListPublishProfiles(string propertyName, AppSettings sourceIndexItem, + internal static void CompareListPublishProfiles(string propertyName, + AppSettings sourceIndexItem, Dictionary>? oldListPublishValue, - Dictionary>? newListPublishValue, List differenceList) + Dictionary>? newListPublishValue, + List differenceList) { if ( oldListPublishValue == null || newListPublishValue?.Count == 0 ) return; if ( oldListPublishValue.Equals(newListPublishValue) ) return; - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newListPublishValue, null); + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newListPublishValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -252,18 +364,22 @@ internal static void CompareListPublishProfiles(string propertyName, AppSettings /// oldBoolValue to compare with newBoolValue /// oldBoolValue to compare with newBoolValue /// list of different values - internal static void CompareBool(string propertyName, AppSettings sourceIndexItem, bool? oldBoolValue, + internal static void CompareBool(string propertyName, AppSettings sourceIndexItem, + bool? oldBoolValue, bool? newBoolValue, List differenceList) { if ( newBoolValue == null ) { return; } + if ( oldBoolValue == newBoolValue ) { return; } - sourceIndexItem.GetType().GetProperty(propertyName)?.SetValue(sourceIndexItem, newBoolValue, null); + + sourceIndexItem.GetType().GetProperty(propertyName) + ?.SetValue(sourceIndexItem, newBoolValue, null); differenceList.Add(propertyName.ToLowerInvariant()); } @@ -288,7 +404,7 @@ internal static void CompareString(string propertyName, AppSettings sourceIndexI } if ( oldStringValue == newStringValue || - ( string.IsNullOrEmpty(newStringValue) ) ) + ( string.IsNullOrEmpty(newStringValue) ) ) { return; } @@ -338,6 +454,5 @@ internal static void CompareInt(string propertyName, AppSettings sourceIndexItem return Array.Find(car.GetType().GetProperties(), pi => pi.Name == propertyName)? .GetValue(car, null); } - } } diff --git a/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs b/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs new file mode 100644 index 0000000000..601a59bb0b --- /dev/null +++ b/starsky/starsky.foundation.platform/Helpers/Compare/AreListsEqual.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace starsky.foundation.platform.Helpers.Compare; + +public static class AreListsEqualHelper +{ + /// + /// Compare two lists + /// + /// First list + /// Second list + /// type of both lists + /// true if same, false if not the same + internal static bool AreListsEqual(List list1, List list2) + { + ArgumentNullException.ThrowIfNull(list1); + ArgumentNullException.ThrowIfNull(list2); + + if ( list1.Count != list2.Count ) + { + return false; + } + + for ( var i = 0; i < list1.Count; i++ ) + { + if ( list1[i]?.Equals(list2[i]) == false ) + { + return false; + } + } + + return true; + } +} diff --git a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs index 8c23c89da1..e65a253f58 100644 --- a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs @@ -372,7 +372,10 @@ public enum ImageFormat mp4 = 50, // archives - zip = 60 + zip = 60, + + // folder + directory = 1000 } diff --git a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs index 664f357657..a386464bdc 100644 --- a/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs +++ b/starsky/starsky.foundation.platform/Helpers/ReadAppSettings.cs @@ -8,7 +8,7 @@ namespace starsky.foundation.platform.Helpers; public static class ReadAppSettings { - internal static async Task Read(string path) + public static async Task Read(string path) { if ( !File.Exists(path) ) { diff --git a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs index 26db9cce03..9b709af273 100644 --- a/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs +++ b/starsky/starsky.foundation.platform/Helpers/SetupAppSettings.cs @@ -11,11 +11,13 @@ using starsky.foundation.platform.Models; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.foundation.platform.Helpers { public static class SetupAppSettings { - public static async Task FirstStepToAddSingleton(ServiceCollection services) + public static async Task FirstStepToAddSingleton( + ServiceCollection services) { services.AddSingleton(new ConfigurationBuilder().Build()); var configurationRoot = await AppSettingsToBuilder(); @@ -35,7 +37,8 @@ public static async Task AppSettingsToBuilder(string[]? args var settings = await MergeJsonFiles(appSettings.BaseDirectoryProject); // Make sure is wrapped in a AppContainer app - var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(new AppContainerAppSettings { App = settings }); + var appContainer = new AppContainerAppSettings { App = settings }; + var utf8Bytes = JsonSerializer.SerializeToUtf8Bytes(appContainer); builder .AddJsonStream(new MemoryStream(utf8Bytes)) @@ -117,6 +120,5 @@ public static AppSettings ConfigurePoCoAppSettings(IServiceCollection services, return serviceProvider.GetRequiredService(); } - } } diff --git a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs index eeec46af9c..dadc18abdc 100644 --- a/starsky/starsky.foundation.platform/Helpers/StringHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/StringHelper.cs @@ -4,10 +4,11 @@ public static class StringHelper { public static string AsciiNullReplacer(string newStringValue) { - return ( newStringValue == "\\0" || newStringValue == "\\\\0" ) ? string.Empty : newStringValue; + return ( newStringValue == "\\0" || newStringValue == "\\\\0" ) + ? string.Empty + : newStringValue; } - public static readonly string AsciiNullChar = "\\\\0"; - + public const string AsciiNullChar = @"\\0"; } } diff --git a/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs b/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs new file mode 100644 index 0000000000..57632d1a7e --- /dev/null +++ b/starsky/starsky.foundation.platform/JsonConverter/EnumListConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace starsky.foundation.platform.JsonConverter; + +/// +/// Enum converter for Lists with Enum into Json +/// +/// Enum +public class EnumListConverter : JsonConverter> where T : struct, Enum +{ + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + if ( reader.TokenType != JsonTokenType.StartArray ) + throw new JsonException(); + + var result = new List(); + + while ( reader.Read() ) + { + if ( reader.TokenType == JsonTokenType.EndArray ) + return result; + + if ( reader.TokenType != JsonTokenType.String ) + throw new JsonException(); + + if ( Enum.TryParse(reader.GetString(), out var enumValue) ) + { + result.Add(enumValue); + } + else + { + throw new JsonException($"Unknown enum value: {reader.GetString()}"); + } + } + + throw new JsonException("Unexpected end of JSON input"); + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach ( var item in value ) + { + writer.WriteStringValue(item.ToString()); + } + + writer.WriteEndArray(); + } +} diff --git a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs index eb8ddfa2ce..552e552fc3 100644 --- a/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs +++ b/starsky/starsky.foundation.platform/Middleware/ContentSecurityPolicyMiddleware.cs @@ -52,7 +52,7 @@ public async Task Invoke(HttpContext httpContext) // Currently not supported in Firefox and Safari (Edge user agent also includes the word Chrome) if ( httpContext.Request.Headers.UserAgent.Contains("Chrome") || - httpContext.Request.Headers.UserAgent.Contains("csp-evaluator") ) + httpContext.Request.Headers.UserAgent.Contains("csp-evaluator") ) { cspHeader += "require-trusted-types-for 'script'; "; } @@ -64,16 +64,16 @@ public async Task Invoke(HttpContext httpContext) // @see: https://www.permissionspolicy.com/ if ( string.IsNullOrEmpty( - httpContext.Response.Headers["Permissions-Policy"]) ) + httpContext.Response.Headers["Permissions-Policy"]) ) { httpContext.Response.Headers .Append("Permissions-Policy", "autoplay=(self), " + - "fullscreen=(self), " + - "geolocation=(self), " + - "picture-in-picture=(self), " + - "clipboard-read=(self), " + - "clipboard-write=(self), " + - "window-placement=(self)"); + "fullscreen=(self), " + + "geolocation=(self), " + + "picture-in-picture=(self), " + + "clipboard-read=(self), " + + "clipboard-write=(self), " + + "window-placement=(self)"); } if ( string.IsNullOrEmpty(httpContext.Response.Headers["Referrer-Policy"]) ) diff --git a/starsky/starsky.foundation.platform/Models/AppSettings.cs b/starsky/starsky.foundation.platform/Models/AppSettings.cs index 83c1730b31..0a246d5f8f 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettings.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettings.cs @@ -8,13 +8,14 @@ using System.Runtime.InteropServices; using System.Text.RegularExpressions; using starsky.foundation.platform.Attributes; +using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.JsonConverter; using TimeZoneConverter; namespace starsky.foundation.platform.Models { - [SuppressMessage("ReSharper", "CA1822")] + [SuppressMessage("Performance", "CA1822:Mark members as static")] public sealed class AppSettings { public AppSettings() @@ -381,7 +382,7 @@ public static void StructureCheck(string? structure) } throw new ArgumentException("(StructureCheck) Structure is not confirm regex - " + - structure); + structure); } /// @@ -587,8 +588,8 @@ public string WebFtp if ( string.IsNullOrEmpty(value) ) return; Uri uriAddress = new Uri(value); if ( uriAddress.UserInfo.Split(":".ToCharArray()).Length == 2 - && uriAddress.Scheme == "ftp" - && uriAddress.LocalPath.Length >= 1 ) + && uriAddress.Scheme == "ftp" + && uriAddress.LocalPath.Length >= 1 ) { _webFtp = value; } @@ -650,8 +651,7 @@ public Dictionary>? PublishProfiles /// Value for AccountRolesDefaultByEmailRegisterOverwrite /// private Dictionary - AccountRolesByEmailRegisterOverwritePrivate - { get; set; } = + AccountRolesByEmailRegisterOverwritePrivate { get; set; } = new Dictionary(); /// @@ -669,7 +669,7 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite { if ( value == null ) return; foreach ( var singleValue in value.Where(singleValue => - AccountRoles.GetAllRoles().Contains(singleValue.Value)) ) + AccountRoles.GetAllRoles().Contains(singleValue.Value)) ) { AccountRolesByEmailRegisterOverwritePrivate.TryAdd( singleValue.Key, singleValue.Value); @@ -731,6 +731,9 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite "/lost+found", "/.stfolder", "/.git" }; + /// + /// Auto Sync on Startup + /// public bool? SyncOnStartup { get; set; } = true; /// @@ -747,6 +750,7 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite /// But it seems a lot of cameras don't do this /// We assume that the standard is followed, and for Camera brands that don't follow the specs use this setting. /// + [PackageTelemetry] public List? VideoUseLocalTime { get; set; } = new List { new CameraMakeModel("Sony", "A58") @@ -757,14 +761,31 @@ public Dictionary? AccountRolesByEmailRegisterOverwrite /// private bool? EnablePackageTelemetryPrivate { get; set; } - /// /// Disable logout buttons in UI /// And hides server specific features that are strange on a local desktop + /// Enable Desktop based features + /// + [PackageTelemetry] + public bool? UseLocalDesktop { get; set; } = false; + + /// + /// Editor by imageFormat /// [PackageTelemetry] - public bool? UseLocalDesktopUi { get; set; } = false; + public List DefaultDesktopEditor { get; set; } = []; + /// + /// When open with desktop app, open the raw or jpeg (Default: NotSet / Jpeg) + /// + [PackageTelemetry] + public CollectionsOpenType.RawJpegMode DesktopCollectionsOpen { get; set; } = + CollectionsOpenType.RawJpegMode.Default; + + /// + /// Number of files to open before confirmation + /// + public int? DesktopEditorAmountBeforeConfirmation { get; set; } /// /// Helps us improve the software @@ -884,7 +905,7 @@ public AppSettings CloneToDisplay() } if ( appSettings.DatabaseType == DatabaseTypeList.Sqlite && - !string.IsNullOrEmpty(userProfileFolder) ) + !string.IsNullOrEmpty(userProfileFolder) ) { appSettings.DatabaseConnection = appSettings.DatabaseConnection.Replace(userProfileFolder, "~"); @@ -896,7 +917,7 @@ public AppSettings CloneToDisplay() } if ( !string.IsNullOrEmpty(appSettings.AppSettingsPath) && - !string.IsNullOrEmpty(userProfileFolder) ) + !string.IsNullOrEmpty(userProfileFolder) ) { appSettings.AppSettingsPath = appSettings.AppSettingsPath.Replace(userProfileFolder, "~"); @@ -905,7 +926,7 @@ public AppSettings CloneToDisplay() if ( appSettings.PublishProfiles != null ) { foreach ( var value in appSettings.PublishProfiles.SelectMany(profile => - profile.Value) ) + profile.Value) ) { ReplaceAppSettingsPublishProfilesCloneToDisplay(value); } @@ -952,7 +973,7 @@ private static void ReplaceAppSettingsPublishProfilesCloneToDisplay( AppSettingsPublishProfiles value) { if ( !string.IsNullOrEmpty(value.Path) && - value.Path != AppSettingsPublishProfiles.GetDefaultPath() ) + value.Path != AppSettingsPublishProfiles.GetDefaultPath() ) { value.Path = CloneToDisplaySecurityWarning; } @@ -1071,7 +1092,7 @@ internal static string ReplaceEnvironmentVariable(string input) public string SqLiteFullPath(string connectionString, string baseDirectoryProject) { if ( DatabaseType == DatabaseTypeList.Mysql && - string.IsNullOrWhiteSpace(connectionString) ) + string.IsNullOrWhiteSpace(connectionString) ) throw new ArgumentException("The 'DatabaseConnection' field is null or empty"); if ( DatabaseType != DatabaseTypeList.Sqlite ) @@ -1092,7 +1113,7 @@ public string SqLiteFullPath(string connectionString, string baseDirectoryProjec if ( baseDirectoryProject.Contains("entityframeworkcore") ) return connectionString; var dataSource = "Data Source=" + baseDirectoryProject + - Path.DirectorySeparatorChar + databaseFileName; + Path.DirectorySeparatorChar + databaseFileName; return dataSource; } @@ -1121,7 +1142,7 @@ internal static void CopyProperties(object source, object destination) var destinationProperty = destinationType.GetProperty(sourceProperty.Name); if ( destinationProperty == null || - !destinationProperty.CanWrite ) + !destinationProperty.CanWrite ) { continue; } diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs b/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs new file mode 100644 index 0000000000..6ca0df653f --- /dev/null +++ b/starsky/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplication.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.JsonConverter; + +namespace starsky.foundation.platform.Models; + +public class AppSettingsDefaultEditorApplication +{ + /// + /// For what type of files + /// + [JsonConverter(typeof(EnumListConverter))] + public List ImageFormats { get; set; } = []; + + /// + /// Path to .exe on windows and .app on Mac OS + /// No check if exists here + /// + public string ApplicationPath { get; set; } = string.Empty; +} diff --git a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs index 483cc02ddb..cc2fcc061d 100644 --- a/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs +++ b/starsky/starsky.foundation.platform/Models/AppSettingsTransferObject.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using starsky.foundation.platform.Enums; + namespace starsky.foundation.platform.Models { /// @@ -10,6 +13,12 @@ public sealed class AppSettingsTransferObject public string? StorageFolder { get; set; } public bool? UseSystemTrash { get; set; } - public bool? UseLocalDesktopUi { get; set; } + public bool? UseLocalDesktop { get; set; } + + public List DefaultDesktopEditor { get; set; } = []; + + public CollectionsOpenType.RawJpegMode DesktopCollectionsOpen { get; set; } = + CollectionsOpenType.RawJpegMode.Default; + } } diff --git a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs index fbaf1df3bf..5ed56c6fc6 100644 --- a/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs +++ b/starsky/starsky.foundation.realtime/Middleware/DisabledWebSocketsMiddleware.cs @@ -11,6 +11,8 @@ namespace starsky.foundation.realtime.Middleware [SuppressMessage("Performance", "IDE0060:UnusedParameter.Local")] public sealed class DisabledWebSocketsMiddleware { + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] public DisabledWebSocketsMiddleware(RequestDelegate next) { } @@ -25,6 +27,7 @@ await webSocket.CloseOutputAsync(WebSocketCloseStatus.MessageTooBig, "Feature toggle disabled", CancellationToken.None); return; } + context.Response.StatusCode = StatusCodes.Status400BadRequest; } } diff --git a/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs b/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs index 1bdcad628f..1ddbeb92fd 100644 --- a/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs +++ b/starsky/starsky.foundation.storage/Interfaces/ISelectorStorage.cs @@ -2,6 +2,9 @@ namespace starsky.foundation.storage.Interfaces { + /// + /// ISelectionStorage + /// public interface ISelectorStorage { IStorage Get(SelectorStorage.StorageServices storageServices); diff --git a/starsky/starsky.foundation.storage/Services/StructureService.cs b/starsky/starsky.foundation.storage/Services/StructureService.cs index 60d71aa6cc..ee0bd5d658 100644 --- a/starsky/starsky.foundation.storage/Services/StructureService.cs +++ b/starsky/starsky.foundation.storage/Services/StructureService.cs @@ -36,8 +36,10 @@ public string ParseFileName(DateTime dateTime, CheckStructureFormat(); var fileName = FilenamesHelper.GetFileName(_structure); var fileNameStructure = PathHelper.PrefixDbSlash(fileName); - var parsedStructuredList = ParseStructure(fileNameStructure, dateTime, fileNameBase, extensionWithoutDot); - return PathHelper.RemovePrefixDbSlash(ApplyStructureRangeToStorage(parsedStructuredList)); + var parsedStructuredList = ParseStructure(fileNameStructure, dateTime, fileNameBase, + extensionWithoutDot); + return PathHelper.RemovePrefixDbSlash( + ApplyStructureRangeToStorage(parsedStructuredList)); } /// @@ -66,7 +68,8 @@ public string ParseSubfolders(DateTime dateTime, string fileNameBase = "", string extensionWithoutDot = "") { CheckStructureFormat(); - var parsedStructuredList = ParseStructure(_structure, dateTime, fileNameBase, extensionWithoutDot); + var parsedStructuredList = + ParseStructure(_structure, dateTime, fileNameBase, extensionWithoutDot); return ApplyStructureRangeToStorage( parsedStructuredList.GetRange(0, parsedStructuredList.Count - 1)); @@ -83,7 +86,6 @@ private string ApplyStructureRangeToStorage(List> parsedStr var parentFolderBuilder = new StringBuilder(); foreach ( var subStructureItem in parsedStructuredList ) { - var currentChildFolderBuilder = new StringBuilder(); currentChildFolderBuilder.Append('/'); @@ -92,21 +94,23 @@ private string ApplyStructureRangeToStorage(List> parsedStr currentChildFolderBuilder.Append(structureItem.Output); } - var parentFolderSubPath = FilenamesHelper.GetParentPath(parentFolderBuilder.ToString()); + var parentFolderSubPath = + FilenamesHelper.GetParentPath(parentFolderBuilder.ToString()); var existParentFolder = _storage.ExistFolder(parentFolderSubPath); // default situation without asterisk or child directory is NOT found if ( !currentChildFolderBuilder.ToString().Contains('*') || !existParentFolder ) { - var currentChildFolderRemovedAsterisk = RemoveAsteriskFromString(currentChildFolderBuilder); + var currentChildFolderRemovedAsterisk = + RemoveAsteriskFromString(currentChildFolderBuilder); parentFolderBuilder.Append(currentChildFolderRemovedAsterisk); continue; } parentFolderBuilder = MatchChildDirectories(parentFolderBuilder, currentChildFolderBuilder); - } + return parentFolderBuilder.ToString(); } @@ -118,10 +122,12 @@ private string ApplyStructureRangeToStorage(List> parsedStr /// the current folder name with asterisk /// other child folder items (item in loop of childDirectories) /// is match - private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder, string p) + private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, + StringBuilder currentChildFolderBuilder, string p) { var matchDirectFolderName = RemoveAsteriskFromString(currentChildFolderBuilder); - if ( matchDirectFolderName != "/" && p == parentFolderBuilder + matchDirectFolderName ) return true; + if ( matchDirectFolderName != "/" && p == parentFolderBuilder + matchDirectFolderName ) + return true; var matchRegex = new Regex( parentFolderBuilder + currentChildFolderBuilder.ToString().Replace("*", ".+"), @@ -136,14 +142,15 @@ private static bool MatchChildFolderSearch(StringBuilder parentFolderBuilder, St /// parent folder (subPath style) /// child folder with asterisk /// SubPath without asterisk - private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, StringBuilder currentChildFolderBuilder) + private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, + StringBuilder currentChildFolderBuilder) { // should return a list of: var childDirectories = _storage.GetDirectories(parentFolderBuilder.ToString()).ToList(); var matchingFoldersPath = childDirectories.Find(p => MatchChildFolderSearch(parentFolderBuilder, currentChildFolderBuilder, p) - ); + ); // When a new folder with asterisk is created if ( matchingFoldersPath == null ) @@ -154,6 +161,7 @@ private StringBuilder MatchChildDirectories(StringBuilder parentFolderBuilder, S { defaultValue = "/default"; } + parentFolderBuilder.Append(defaultValue); return parentFolderBuilder; } @@ -183,13 +191,14 @@ private static string RemoveAsteriskFromString(StringBuilder input) private void CheckStructureFormat() { if ( !string.IsNullOrEmpty(_structure) && - _structure.StartsWith('/') && _structure.EndsWith(".ext") && - _structure != "/.ext" ) + _structure.StartsWith('/') && _structure.EndsWith(".ext") && + _structure != "/.ext" ) { return; } - throw new FieldAccessException("Structure is not right formatted, please read the documentation"); + throw new FieldAccessException( + "Structure is not right formatted, please read the documentation"); } /// @@ -197,7 +206,8 @@ private void CheckStructureFormat() /// @see: https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings /// Not escaped regex: \\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3}) /// - const string DateRegexPattern = "\\\\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3})"; + const string DateRegexPattern = + "\\\\?(d{1,4}|f{1,6}|F{1,6}|g{1,2}|h{1,2}|H{1,2}|K|m{1,2}|M{1,4}|s{1,2}|t{1,2}|y{1,5}|z{1,3})"; /// /// Parse the dateTime structure input to the dateTime provided @@ -207,7 +217,8 @@ private void CheckStructureFormat() /// source name, can be used in the options /// fileExtension without dot /// Object with Structure Range output - private static List> ParseStructure(string structure, DateTime dateTime, + private static List> ParseStructure(string structure, + DateTime dateTime, string fileNameBase = "", string extensionWithoutDot = "") { var structureList = structure.Split('/'); @@ -219,8 +230,8 @@ private static List> ParseStructure(string structure, DateT var matchCollection = new Regex(DateRegexPattern + "|{filenamebase}|\\*|.ext|.", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) - .Matches(structureItem); + RegexOptions.None, TimeSpan.FromMilliseconds(200)) + .Matches(structureItem); var matchList = new List(); foreach ( Match match in matchCollection ) @@ -230,7 +241,8 @@ private static List> ParseStructure(string structure, DateT Pattern = match.Value, Start = match.Index, End = match.Index + match.Length, - Output = OutputStructureRangeItemParser(match.Value, dateTime, fileNameBase, extensionWithoutDot) + Output = OutputStructureRangeItemParser(match.Value, dateTime, + fileNameBase, extensionWithoutDot) }); } @@ -252,13 +264,14 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da string fileNameBase, string extensionWithoutDot = "") { // allow only full word matches (so .ext is no match) - MatchCollection matchCollection = new Regex(DateRegexPattern, + var matchCollection = new Regex(DateRegexPattern, RegexOptions.None, TimeSpan.FromMilliseconds(100)).Matches(pattern); foreach ( Match match in matchCollection ) { // Ignore escaped items - if ( !match.Value.StartsWith('\\') && match.Index == 0 && match.Length == pattern.Length ) + if ( !match.Value.StartsWith('\\') && match.Index == 0 && + match.Length == pattern.Length ) { return dateTime.ToString(pattern, CultureInfo.InvariantCulture); } @@ -270,7 +283,9 @@ private static string OutputStructureRangeItemParser(string pattern, DateTime da case "{filenamebase}": return fileNameBase; case ".ext": - return string.IsNullOrEmpty(extensionWithoutDot) ? ".unknown" : $".{extensionWithoutDot}"; + return string.IsNullOrEmpty(extensionWithoutDot) + ? ".unknown" + : $".{extensionWithoutDot}"; default: return pattern.Replace("\\", string.Empty); } diff --git a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs index 98339af010..f44ea093c3 100644 --- a/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs +++ b/starsky/starsky.foundation.sync/SyncServices/SyncFolder.cs @@ -145,6 +145,7 @@ internal async Task CompareFolderListAndFixMissingFolders(List subPaths, await _query.AddItemAsync(new FileIndexItem(path) { IsDirectory = true, + ImageFormat = ExtensionRolesHelper.ImageFormat.directory, AddToDatabase = DateTime.UtcNow, ColorClass = ColorClassParser.Color.None }); diff --git a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs index 39d619fcff..97039e6441 100644 --- a/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs +++ b/starsky/starsky.foundation.sync/WatcherHelpers/SyncWatcherConnector.cs @@ -13,6 +13,7 @@ using starsky.foundation.database.Models; using starsky.foundation.database.Query; using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.JsonConverter; using starsky.foundation.platform.Models; @@ -114,6 +115,7 @@ private async Task> SyncTaskInternal( syncData.Add(new FileIndexItem(_appSettings.FullPathToDatabaseStyle(fullFilePath)) { IsDirectory = true, + ImageFormat = ExtensionRolesHelper.ImageFormat.directory, Status = FileIndexItem.ExifStatus.NotFoundSourceMissing }); diff --git a/starsky/starsky.foundation.platform/Helpers/EnumHelper.cs b/starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs similarity index 92% rename from starsky/starsky.foundation.platform/Helpers/EnumHelper.cs rename to starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs index e329bd7fa3..b799c3daaf 100644 --- a/starsky/starsky.foundation.platform/Helpers/EnumHelper.cs +++ b/starsky/starsky.foundation.writemeta/Helpers/EnumHelper.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Reflection; -namespace starsky.foundation.platform.Helpers +namespace starsky.foundation.writemeta.Helpers { public static class EnumHelper { diff --git a/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs b/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs new file mode 100644 index 0000000000..583bf9205a --- /dev/null +++ b/starsky/starsky.project.web/Attributes/ExcludeFromCoverageAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace starsky.project.web.Attributes +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)] + public sealed class ExcludeFromCoverageAttribute : Attribute + { + } +} diff --git a/starsky/starskycore/Helpers/MimeHelper.cs b/starsky/starsky.project.web/Helpers/MimeHelper.cs similarity index 99% rename from starsky/starskycore/Helpers/MimeHelper.cs rename to starsky/starsky.project.web/Helpers/MimeHelper.cs index 8d3c9ac17a..d1bcb9363c 100644 --- a/starsky/starskycore/Helpers/MimeHelper.cs +++ b/starsky/starsky.project.web/Helpers/MimeHelper.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.IO; -namespace starskycore.Helpers +namespace starsky.project.web.Helpers { public static class MimeHelper { diff --git a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs b/starsky/starsky.project.web/Helpers/PortProgramHelper.cs similarity index 69% rename from starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs rename to starsky/starsky.project.web/Helpers/PortProgramHelper.cs index 292e1e5b75..7b64e6c1d7 100644 --- a/starsky/starsky.foundation.platform/Helpers/PortProgramHelper.cs +++ b/starsky/starsky.project.web/Helpers/PortProgramHelper.cs @@ -2,13 +2,18 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using starsky.foundation.platform.Helpers; -namespace starsky.foundation.platform.Helpers; +[assembly: InternalsVisibleTo("starskytest")] + +namespace starsky.project.web.Helpers; public static class PortProgramHelper { - public static async Task SetEnvPortAspNetUrlsAndSetDefault(string[] args, string appSettingsPath) + public static async Task SetEnvPortAspNetUrlsAndSetDefault(string[] args, + string appSettingsPath) { if ( await SkipForAppSettingsJsonFile(appSettingsPath) ) { @@ -23,14 +28,14 @@ internal static async Task SkipForAppSettingsJsonFile(string appSettingsPa { var appContainer = await ReadAppSettings.Read(appSettingsPath); if ( appContainer?.Kestrel?.Endpoints?.Http?.Url == null && - appContainer?.Kestrel?.Endpoints?.Https?.Url == null ) + appContainer?.Kestrel?.Endpoints?.Https?.Url == null ) { return false; } Console.WriteLine("Kestrel Endpoints are set in appsettings.json, " + - "this results in skip setting the PORT and default " + - "ASPNETCORE_URLS environment variable"); + "this results in skip setting the PORT and default " + + "ASPNETCORE_URLS environment variable"); return true; } @@ -40,7 +45,7 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args) var portString = Environment.GetEnvironmentVariable("PORT"); if ( args.Contains("--urls") || string.IsNullOrEmpty(portString) - || !int.TryParse(portString, out var port) ) return; + || !int.TryParse(portString, out var port) ) return; SetEnvironmentVariableForPort(port); } @@ -49,7 +54,7 @@ internal static void SetEnvPortAspNetUrls(IEnumerable args) private static void SetEnvironmentVariableForPort(int port) { Console.WriteLine($"Set port from environment variable: {port} " + - $"\nPro tip: Its recommended to use a https proxy like nginx or traefik"); + $"\nPro tip: Its recommended to use a https proxy like nginx or traefik"); Environment.SetEnvironmentVariable("ASPNETCORE_URLS", $"http://*:{port}"); } @@ -57,6 +62,7 @@ internal static void SetDefaultAspNetCoreUrls(IEnumerable args) { var aspNetCoreUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); if ( args.Contains("--urls") || !string.IsNullOrEmpty(aspNetCoreUrls) ) return; - Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "http://localhost:4000;https://localhost:4001"); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", + "http://localhost:4000;https://localhost:4001"); } } diff --git a/starsky/starskycore/Helpers/ToSQL.cs_debug b/starsky/starsky.project.web/Helpers/ToSQL.cs_debug similarity index 96% rename from starsky/starskycore/Helpers/ToSQL.cs_debug rename to starsky/starsky.project.web/Helpers/ToSQL.cs_debug index 1c687c6264..f8af8d6248 100644 --- a/starsky/starskycore/Helpers/ToSQL.cs_debug +++ b/starsky/starsky.project.web/Helpers/ToSQL.cs_debug @@ -4,7 +4,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -namespace starskycore.Helpers +namespace starsky.project.web.Helpers { public static class ToSqlExtension { diff --git a/starsky/starskycore/ViewModels/ArchiveViewModel.cs b/starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs similarity index 97% rename from starsky/starskycore/ViewModels/ArchiveViewModel.cs rename to starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs index 2c86a3a9b0..e9fccb8eff 100644 --- a/starsky/starskycore/ViewModels/ArchiveViewModel.cs +++ b/starsky/starsky.project.web/ViewModels/ArchiveViewModel.cs @@ -3,7 +3,7 @@ using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; -namespace starskycore.ViewModels +namespace starsky.project.web.ViewModels { [SuppressMessage("Performance", "CA1822:Mark members as static")] public sealed class ArchiveViewModel diff --git a/starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs b/starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs similarity index 53% rename from starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs rename to starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs index b52a03b918..34f731c7fd 100644 --- a/starsky/starskycore/ViewModels/EnvFeaturesViewModel.cs +++ b/starsky/starsky.project.web/ViewModels/EnvFeaturesViewModel.cs @@ -1,8 +1,7 @@ -namespace starskycore.ViewModels; +namespace starsky.project.web.ViewModels; public class EnvFeaturesViewModel { - /// /// Trash is very dependent on the OS /// @@ -11,5 +10,10 @@ public class EnvFeaturesViewModel /// /// Enable or disable some features on the frontend /// - public bool UseLocalDesktopUi { get; set; } + public bool UseLocalDesktop { get; set; } + + /// + /// Is supported and enabled in the feature toggle + /// + public bool OpenEditorEnabled { get; set; } } diff --git a/starsky/starskycore/ViewModels/HealthView.cs b/starsky/starsky.project.web/ViewModels/HealthView.cs similarity index 93% rename from starsky/starskycore/ViewModels/HealthView.cs rename to starsky/starsky.project.web/ViewModels/HealthView.cs index e248dd584e..32929e176f 100644 --- a/starsky/starskycore/ViewModels/HealthView.cs +++ b/starsky/starsky.project.web/ViewModels/HealthView.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace starskycore.ViewModels +namespace starsky.project.web.ViewModels { public sealed class HealthView { diff --git a/starsky/starskycore/ViewModels/MetricsDebugViewModel.cs b/starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs similarity index 66% rename from starsky/starskycore/ViewModels/MetricsDebugViewModel.cs rename to starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs index 83123d93bd..93293b5085 100644 --- a/starsky/starskycore/ViewModels/MetricsDebugViewModel.cs +++ b/starsky/starsky.project.web/ViewModels/MetricsDebugViewModel.cs @@ -1,4 +1,4 @@ -namespace starskycore.ViewModels; +namespace starsky.project.web.ViewModels; public class MetricsDebugViewModel { diff --git a/starsky/starskycore/ViewModels/SyncViewModel.cs b/starsky/starsky.project.web/ViewModels/SyncViewModel.cs similarity index 87% rename from starsky/starskycore/ViewModels/SyncViewModel.cs rename to starsky/starsky.project.web/ViewModels/SyncViewModel.cs index 2f33ba4bf9..0e3c341b7c 100644 --- a/starsky/starskycore/ViewModels/SyncViewModel.cs +++ b/starsky/starsky.project.web/ViewModels/SyncViewModel.cs @@ -1,7 +1,7 @@ -using starsky.foundation.database.Models; using System.Text.Json.Serialization; +using starsky.foundation.database.Models; -namespace starskycore.ViewModels +namespace starsky.project.web.ViewModels { public sealed class SyncViewModel { diff --git a/starsky/starskycore/starskycore.csproj b/starsky/starsky.project.web/starsky.project.web.csproj similarity index 93% rename from starsky/starskycore/starskycore.csproj rename to starsky/starsky.project.web/starsky.project.web.csproj index 04ae30444c..91466ff82f 100644 --- a/starsky/starskycore/starskycore.csproj +++ b/starsky/starsky.project.web/starsky.project.web.csproj @@ -10,6 +10,7 @@ SYSTEM_TEXT_ENABLED Full enable + starsky.project.web diff --git a/starsky/starsky.sln b/starsky/starsky.sln index 5623f89bd0..ef8b5bbfa1 100644 --- a/starsky/starsky.sln +++ b/starsky/starsky.sln @@ -13,7 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskywebhtmlcli", "starsk EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskygeocli", "starskygeocli\starskygeocli.csproj", "{EF96F7C8-4832-4606-8F5C-B1423FEE83B8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskycore", "starskycore\starskycore.csproj", "{A90751E7-2F4D-44AA-8507-DDE5F980DBBB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.project.web", "starsky.project.web\starsky.project.web.csproj", "{A90751E7-2F4D-44AA-8507-DDE5F980DBBB}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starskywebftpcli", "starskywebftpcli\starskywebftpcli.csproj", "{F8CE092D-F296-4B04-B013-EE5FCD4A8B3B}" EndProject @@ -164,6 +164,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.trash", "st EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.settings", "starsky.feature.settings\starsky.feature.settings.csproj", "{F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "starsky.feature.desktop", "starsky.feature.desktop\starsky.feature.desktop.csproj", "{B88C2815-D154-4C6D-AE37-2E150AEBF73D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -362,6 +364,10 @@ Global {F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2C4C9DE-22A1-4B34-AC1D-0F08353E0742}.Release|Any CPU.Build.0 = Release|Any CPU + {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B88C2815-D154-4C6D-AE37-2E150AEBF73D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -421,5 +427,6 @@ Global {0072F697-4E18-4B5F-80DF-530361D3E847} = {1C1EB4A5-08D0-4014-AE1F-962642A4E5D3} {A62C129C-5D0C-4A0A-B5AA-261E041FF55D} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A} {F2C4C9DE-22A1-4B34-AC1D-0F08353E0742} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A} + {B88C2815-D154-4C6D-AE37-2E150AEBF73D} = {4B9276C3-651E-48D3-B3A7-3F4D74F3D01A} EndGlobalSection EndGlobal diff --git a/starsky/starsky/Controllers/AccountController.cs b/starsky/starsky/Controllers/AccountController.cs index c5e26c60c2..b723387ccb 100644 --- a/starsky/starsky/Controllers/AccountController.cs +++ b/starsky/starsky/Controllers/AccountController.cs @@ -23,12 +23,14 @@ public sealed class AccountController : Controller private readonly IAntiforgery _antiForgery; private readonly IStorage _storageHostFullPathFilesystem; - public AccountController(IUserManager userManager, AppSettings appSettings, IAntiforgery antiForgery, ISelectorStorage selectorStorage) + public AccountController(IUserManager userManager, AppSettings appSettings, + IAntiforgery antiForgery, ISelectorStorage selectorStorage) { _userManager = userManager; _appSettings = appSettings; _antiForgery = antiForgery; - _storageHostFullPathFilesystem = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _storageHostFullPathFilesystem = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); } /// @@ -76,9 +78,7 @@ public async Task Status() var model = new UserIdentifierStatusModel { - Name = currentUser.Name, - Id = currentUser.Id, - Created = currentUser.Created, + Name = currentUser.Name, Id = currentUser.Id, Created = currentUser.Created, }; var credentials = _userManager.GetCredentialsByUserId(currentUser.Id); @@ -88,9 +88,13 @@ public async Task Status() model.CredentialTypeIds = null; return Json(model); } - + model.CredentialsIdentifiers?.Add(credentials.Identifier!); model.CredentialTypeIds?.Add(credentials.CredentialTypeId); + + var role = await _userManager.GetRoleAsync(currentUser.Id); + model.RoleCode = role?.Code; + return Json(model); } @@ -105,6 +109,7 @@ public async Task Status() [ProducesResponseType(200)] [Produces("text/html")] [SuppressMessage("ReSharper", "UnusedParameter.Global")] + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] [AllowAnonymous] public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null) { @@ -112,7 +117,8 @@ public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null) var clientApp = Path.Combine(_appSettings.BaseDirectoryProject, "clientapp", "build", "index.html"); - if ( !_storageHostFullPathFilesystem.ExistFile(clientApp) ) return Content("Please check if the client code exist"); + if ( !_storageHostFullPathFilesystem.ExistFile(clientApp) ) + return Content("Please check if the client code exist"); return PhysicalFile(clientApp, "text/html"); } @@ -137,7 +143,8 @@ public IActionResult LoginGet(string? returnUrl = null, bool? fromLogout = null) [AllowAnonymous] public async Task LoginPost(LoginViewModel model) { - ValidateResult validateResult = await _userManager.ValidateAsync("Email", model.Email, model.Password); + ValidateResult validateResult = + await _userManager.ValidateAsync("Email", model.Email, model.Password); if ( !validateResult.Success ) { @@ -146,6 +153,7 @@ public async Task LoginPost(LoginViewModel model) { Response.StatusCode = 423; } + return Json("Login failed"); } @@ -186,7 +194,8 @@ public IActionResult Logout(string? returnUrl = null) { _userManager.SignOut(HttpContext); // fromLogout is used in middleware - return RedirectToAction(nameof(LoginGet), new { ReturnUrl = returnUrl, fromLogout = true }); + return RedirectToAction(nameof(LoginGet), + new { ReturnUrl = returnUrl, fromLogout = true }); } /// @@ -211,7 +220,7 @@ public async Task ChangeSecret(ChangePasswordViewModel model) } if ( !ModelState.IsValid || - model.ChangedPassword != model.ChangedConfirmPassword ) + model.ChangedPassword != model.ChangedConfirmPassword ) { return BadRequest("Model is not correct"); } @@ -282,7 +291,8 @@ public async Task Register(RegisterViewModel model) private async Task IsAccountRegisterClosed(bool userIdentityIsAuthenticated) { if ( userIdentityIsAuthenticated ) return false; - return _appSettings.IsAccountRegisterOpen != true && ( await _userManager.AllUsersAsync() ).Users.Count != 0; + return _appSettings.IsAccountRegisterOpen != true && + ( await _userManager.AllUsersAsync() ).Users.Count != 0; } /// @@ -305,7 +315,7 @@ public async Task RegisterStatus() } if ( !await IsAccountRegisterClosed( - User.Identity?.IsAuthenticated == true) ) + User.Identity?.IsAuthenticated == true) ) { return Json("RegisterStatus open"); } @@ -329,6 +339,5 @@ public IActionResult Permissions() var claims = User.Claims.Where(p => p.Type == "Permission").Select(p => p.Value); return Json(claims); } - } } diff --git a/starsky/starsky/Controllers/AllowedTypesController.cs b/starsky/starsky/Controllers/AllowedTypesController.cs index a695ef484a..b60eb91e9f 100644 --- a/starsky/starsky/Controllers/AllowedTypesController.cs +++ b/starsky/starsky/Controllers/AllowedTypesController.cs @@ -3,14 +3,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using starsky.foundation.platform.Helpers; -using starskycore.Helpers; +using starsky.project.web.Helpers; namespace starsky.Controllers { [Authorize] public sealed class AllowedTypesController : Controller { - /// /// A (string) list of allowed MIME-types ExtensionSyncSupportedList /// @@ -22,7 +21,8 @@ public sealed class AllowedTypesController : Controller [Produces("application/json")] public IActionResult AllowedTypesMimetypeSync() { - var mimeTypes = ExtensionRolesHelper.ExtensionSyncSupportedList.Select(MimeHelper.GetMimeType).ToHashSet(); + var mimeTypes = ExtensionRolesHelper.ExtensionSyncSupportedList + .Select(MimeHelper.GetMimeType).ToHashSet(); return Json(mimeTypes); } @@ -38,7 +38,8 @@ public IActionResult AllowedTypesMimetypeSync() [Produces("application/json")] public IActionResult AllowedTypesMimetypeSyncThumb() { - var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList.Select(MimeHelper.GetMimeType).ToHashSet(); + var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList + .Select(MimeHelper.GetMimeType).ToHashSet(); return Json(mimeTypes); } diff --git a/starsky/starsky/Controllers/AppSettingsController.cs b/starsky/starsky/Controllers/AppSettingsController.cs index 783f8c4b03..ad18e0b25b 100644 --- a/starsky/starsky/Controllers/AppSettingsController.cs +++ b/starsky/starsky/Controllers/AppSettingsController.cs @@ -38,6 +38,8 @@ public AppSettingsController(AppSettings appSettings, public IActionResult Env() { var appSettings = _appSettings.CloneToDisplay(); + + // For end-to-end testing if ( Request != null! && Request.Headers.Any(p => p.Key == "x-force-html") ) { Response.Headers.ContentType = "text/html; charset=utf-8"; diff --git a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs index fd7d9dfb06..a41dcee8c6 100644 --- a/starsky/starsky/Controllers/AppSettingsFeaturesController.cs +++ b/starsky/starsky/Controllers/AppSettingsFeaturesController.cs @@ -1,8 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using starsky.feature.desktop.Interfaces; using starsky.feature.trash.Interfaces; using starsky.foundation.platform.Models; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starsky.Controllers; @@ -10,10 +11,14 @@ public class AppSettingsFeaturesController : Controller { private readonly IMoveToTrashService _moveToTrashService; private readonly AppSettings _appSettings; + private readonly IOpenEditorDesktopService _openEditorDesktopService; - public AppSettingsFeaturesController(IMoveToTrashService moveToTrashService, AppSettings appSettings) + public AppSettingsFeaturesController(IMoveToTrashService moveToTrashService, + IOpenEditorDesktopService openEditorDesktopService, + AppSettings appSettings) { _moveToTrashService = moveToTrashService; + _openEditorDesktopService = openEditorDesktopService; _appSettings = appSettings; } @@ -36,7 +41,8 @@ public IActionResult FeaturesView() var shortAppSettings = new EnvFeaturesViewModel { SystemTrashEnabled = _moveToTrashService.IsEnabled(), - UseLocalDesktopUi = _appSettings.UseLocalDesktopUi == true + UseLocalDesktop = _appSettings.UseLocalDesktop == true, + OpenEditorEnabled = _openEditorDesktopService.IsEnabled() }; return Json(shortAppSettings); diff --git a/starsky/starsky/Controllers/DesktopEditorController.cs b/starsky/starsky/Controllers/DesktopEditorController.cs new file mode 100644 index 0000000000..0cfdbbd1ce --- /dev/null +++ b/starsky/starsky/Controllers/DesktopEditorController.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using starsky.feature.desktop.Interfaces; +using starsky.feature.desktop.Models; + +namespace starsky.Controllers; + +[Authorize] +public class DesktopEditorController : Controller +{ + private readonly IOpenEditorDesktopService _openEditorDesktopService; + + public DesktopEditorController(IOpenEditorDesktopService openEditorDesktopService) + { + _openEditorDesktopService = openEditorDesktopService; + } + + /// + /// Open a file in the default editor or a specific editor on the desktop + /// + /// single or multiple subPaths + /// to combine files with the same name before the extension + /// + /// returns a list of items from the database + /// list with no content + /// subPath not found in the database + /// User unauthorized + [HttpPost("/api/desktop-editor/open")] + [Produces("application/json")] + [ProducesResponseType(typeof(List), 200)] + [ProducesResponseType(typeof(List), 206)] + [ProducesResponseType(typeof(string), 400)] + [ProducesResponseType(401)] + public async Task OpenAsync( + string f = "", + bool collections = true) + { + var (success, status, list) = + await _openEditorDesktopService.OpenAsync(f, collections); + + switch ( success ) + { + case null: + return BadRequest(status); + case false: + HttpContext.Response.StatusCode = 206; + break; + } + + return Json(list); + } + + + /// + /// Check the amount of files to open before + /// + /// single or multiple subPaths + /// + /// bool, true is no confirmation, false is ask confirmation + /// User unauthorized + [HttpPost("/api/desktop-editor/amount-confirmation")] + [Produces("application/json")] + [ProducesResponseType(typeof(bool), 200)] + [ProducesResponseType(401)] + public IActionResult OpenAmountConfirmationChecker(string f) + { + var result = _openEditorDesktopService.OpenAmountConfirmationChecker(f); + return Json(result); + } +} diff --git a/starsky/starsky/Controllers/DiskController.cs b/starsky/starsky/Controllers/DiskController.cs index 855b1b3fa0..e8c8666dc4 100644 --- a/starsky/starsky/Controllers/DiskController.cs +++ b/starsky/starsky/Controllers/DiskController.cs @@ -13,7 +13,7 @@ using starsky.foundation.realtime.Interfaces; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Storage; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starsky.Controllers { @@ -62,11 +62,9 @@ public async Task Mkdir(string f) foreach ( var subPath in inputFilePaths.Select(PathHelper.RemoveLatestSlash) ) { - var toAddStatus = new SyncViewModel { - FilePath = subPath, - Status = FileIndexItem.ExifStatus.Ok + FilePath = subPath, Status = FileIndexItem.ExifStatus.Ok }; if ( _iStorage.ExistFolder(subPath) ) @@ -78,7 +76,7 @@ public async Task Mkdir(string f) await _query.AddItemAsync(new FileIndexItem(subPath) { - IsDirectory = true + IsDirectory = true, ImageFormat = ExtensionRolesHelper.ImageFormat.directory }); // add to fs @@ -102,12 +100,12 @@ await _query.AddItemAsync(new FileIndexItem(subPath) /// SyncViewModel /// optional debug name /// Completed send of Socket SendToAllAsync - private async Task SyncMessageToSocket(IEnumerable syncResultsList, ApiNotificationType type = ApiNotificationType.Unknown) + private async Task SyncMessageToSocket(IEnumerable syncResultsList, + ApiNotificationType type = ApiNotificationType.Unknown) { var list = syncResultsList.Select(t => new FileIndexItem(t.FilePath) { - Status = t.Status, - IsDirectory = true + Status = t.Status, IsDirectory = true }).ToList(); var webSocketResponse = new ApiNotificationResponseModel< @@ -132,7 +130,8 @@ private async Task SyncMessageToSocket(IEnumerable syncResultsLis [ProducesResponseType(typeof(List), 404)] [HttpPost("/api/disk/rename")] [Produces("application/json")] - public async Task Rename(string f, string to, bool collections = true, bool currentStatus = true) + public async Task Rename(string f, string to, bool collections = true, + bool currentStatus = true) { if ( string.IsNullOrEmpty(f) ) { @@ -146,14 +145,16 @@ public async Task Rename(string f, string to, bool collections = return NotFound(rename); var webSocketResponse = - new ApiNotificationResponseModel>(rename, ApiNotificationType.Rename); + new ApiNotificationResponseModel>(rename, + ApiNotificationType.Rename); await _notificationQuery.AddNotification(webSocketResponse); await _connectionsService.SendToAllAsync(webSocketResponse, CancellationToken.None); - return Json(currentStatus ? rename.Where(p => p.Status - != FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList() : rename); + return Json(currentStatus + ? rename.Where(p => p.Status + != FileIndexItem.ExifStatus.NotFoundSourceMissing).ToList() + : rename); } - } } diff --git a/starsky/starsky/Controllers/DownloadPhotoController.cs b/starsky/starsky/Controllers/DownloadPhotoController.cs index ee409f1f02..509b7f9924 100644 --- a/starsky/starsky/Controllers/DownloadPhotoController.cs +++ b/starsky/starsky/Controllers/DownloadPhotoController.cs @@ -10,7 +10,7 @@ using starsky.foundation.storage.Storage; using starsky.foundation.thumbnailgeneration.Interfaces; using starsky.Helpers; -using starskycore.Helpers; +using starsky.project.web.Helpers; namespace starsky.Controllers { diff --git a/starsky/starsky/Controllers/ExportController.cs b/starsky/starsky/Controllers/ExportController.cs index 0dd68b2d14..22d2d98d64 100644 --- a/starsky/starsky/Controllers/ExportController.cs +++ b/starsky/starsky/Controllers/ExportController.cs @@ -10,7 +10,7 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Storage; using starsky.foundation.worker.Interfaces; -using starskycore.Helpers; +using starsky.project.web.Helpers; namespace starsky.Controllers { diff --git a/starsky/starsky/Controllers/HealthController.cs b/starsky/starsky/Controllers/HealthController.cs index ceb03fb6e6..98f630648a 100644 --- a/starsky/starsky/Controllers/HealthController.cs +++ b/starsky/starsky/Controllers/HealthController.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.VersionHelpers; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; [assembly: InternalsVisibleTo("starskytest")] @@ -60,9 +60,9 @@ internal async Task CheckHealthAsyncWithTimeout(int timeoutTime = try { if ( _cache != null && - _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) && - objectHealthStatus is HealthReport healthStatus && - healthStatus.Status == HealthStatus.Healthy ) + _cache.TryGetValue(healthControllerCacheKey, out var objectHealthStatus) && + objectHealthStatus is HealthReport healthStatus && + healthStatus.Status == HealthStatus.Healthy ) { return healthStatus; } @@ -132,7 +132,7 @@ private static HealthView CreateHealthEntryLog(HealthReport result) Name = key, IsHealthy = value.Status == HealthStatus.Healthy, Description = value.Description + value.Exception?.Message + - value.Exception?.StackTrace + value.Exception?.StackTrace } ); } diff --git a/starsky/starsky/Controllers/HomeController.cs b/starsky/starsky/Controllers/HomeController.cs index 9d77106361..2b14477c3a 100644 --- a/starsky/starsky/Controllers/HomeController.cs +++ b/starsky/starsky/Controllers/HomeController.cs @@ -10,6 +10,7 @@ using starsky.Helpers; [assembly: InternalsVisibleTo("starskytest")] + namespace starsky.Controllers { [Authorize] @@ -35,6 +36,7 @@ public HomeController(AppSettings appSettings, IAntiforgery antiForgery) [Produces("text/html")] [ProducesResponseType(200)] [ProducesResponseType(401)] + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] public IActionResult Index(string f = "") { new AntiForgeryCookie(_antiForgery).SetAntiForgeryCookie(HttpContext); @@ -63,10 +65,11 @@ public IActionResult SearchPost(string t = "", int p = 0) // Added filter to prevent redirects based on tainted, user-controlled data // unescaped: ^[a-zA-Z0-9_\-+"'/=:,\.>< ]+$ if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:,\\.>< ]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) { return BadRequest("`t` is not allowed"); } + return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}")); } @@ -102,10 +105,11 @@ public IActionResult Search(string t = "", int p = 0) // Added filter to prevent redirects based on tainted, user-controlled data // unescaped: ^[a-zA-Z0-9_\-+"'/=:>< ]+$ if ( !Regex.IsMatch(t, "^[a-zA-Z0-9_\\-+\"'/=:>< ]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) { return BadRequest("`t` is not allowed"); } + return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/search?t={t}&p={p}")); } @@ -128,6 +132,7 @@ public IActionResult Trash(int p = 0) { return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/trash?p={p}")); } + return PhysicalFile(_clientApp, "text/html"); } @@ -147,6 +152,7 @@ public IActionResult Import() { return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/import")); } + return PhysicalFile(_clientApp, "text/html"); } @@ -166,6 +172,7 @@ public IActionResult Preferences() { return Redirect(AppendPathBasePrefix(Request.PathBase.Value, $"/preferences")); } + return PhysicalFile(_clientApp, "text/html"); } @@ -181,6 +188,7 @@ public IActionResult Preferences() [Produces("text/html")] [ProducesResponseType(200)] [SuppressMessage("ReSharper", "UnusedParameter.Global")] + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] public IActionResult Register(string? returnUrl = null) { new AntiForgeryCookie(_antiForgery).SetAntiForgeryCookie(HttpContext); @@ -190,10 +198,13 @@ public IActionResult Register(string? returnUrl = null) internal static string AppendPathBasePrefix(string? requestPathBase, string url) { return requestPathBase?.Equals("/starsky", - StringComparison.InvariantCultureIgnoreCase) == true ? $"/starsky{url}" : url; + StringComparison.InvariantCultureIgnoreCase) == true + ? $"/starsky{url}" + : url; } - internal static bool IsCaseSensitiveRedirect(string? expectedRequestPath, string? requestPathValue) + internal static bool IsCaseSensitiveRedirect(string? expectedRequestPath, + string? requestPathValue) { return expectedRequestPath != requestPathValue; } diff --git a/starsky/starsky/Controllers/IndexController.cs b/starsky/starsky/Controllers/IndexController.cs index b240aee0db..0536625a81 100644 --- a/starsky/starsky/Controllers/IndexController.cs +++ b/starsky/starsky/Controllers/IndexController.cs @@ -6,7 +6,7 @@ using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starsky.Controllers { diff --git a/starsky/starsky/Controllers/MetricsDebugController.cs b/starsky/starsky/Controllers/MetricsDebugController.cs index 9108760e2a..8318079bc3 100644 --- a/starsky/starsky/Controllers/MetricsDebugController.cs +++ b/starsky/starsky/Controllers/MetricsDebugController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using starsky.foundation.worker.CpuEventListener.Interfaces; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starsky.Controllers; @@ -18,9 +18,6 @@ public MetricsDebugController(ICpuUsageListener cpuUsageListener) public IActionResult Index() { - return Json(new MetricsDebugViewModel - { - CpuUsageMean = _cpuUsageListener.CpuUsageMean, - }); + return Json(new MetricsDebugViewModel { CpuUsageMean = _cpuUsageListener.CpuUsageMean, }); } } diff --git a/starsky/starsky/Controllers/ThumbnailController.cs b/starsky/starsky/Controllers/ThumbnailController.cs index e00ab4c645..8c97aaf858 100644 --- a/starsky/starsky/Controllers/ThumbnailController.cs +++ b/starsky/starsky/Controllers/ThumbnailController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; @@ -13,7 +14,7 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; using starsky.foundation.storage.Storage; -using starskycore.Helpers; +using starsky.project.web.Helpers; namespace starsky.Controllers { @@ -53,22 +54,29 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f) // Restrict the fileHash to letters and digits only // I/O function calls should not be vulnerable to path injection attacks if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(200)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(200)) ) { return BadRequest(); } if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)) ) { - var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)); - Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Small.ToString())); + var stream = + _thumbnailStorage.ReadStream( + ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)); + Response.Headers.TryAdd("x-image-size", + new StringValues(ThumbnailSize.Small.ToString())); return File(stream, "image/jpeg"); } - if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)) ) + if ( _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)) ) { - var stream = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)); - Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.TinyMeta.ToString())); + var stream = + _thumbnailStorage.ReadStream( + ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)); + Response.Headers.TryAdd("x-image-size", + new StringValues(ThumbnailSize.TinyMeta.ToString())); return File(stream, "image/jpeg"); } @@ -78,8 +86,10 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f) return NotFound("hash not found"); } - var streamDefaultThumbnail = _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)); - Response.Headers.TryAdd("x-image-size", new StringValues(ThumbnailSize.Large.ToString())); + var streamDefaultThumbnail = + _thumbnailStorage.ReadStream(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)); + Response.Headers.TryAdd("x-image-size", + new StringValues(ThumbnailSize.Large.ToString())); return File(streamDefaultThumbnail, "image/jpeg"); } @@ -101,7 +111,6 @@ public IActionResult ThumbnailSmallOrTinyMeta(string f) [ProducesResponseType( 400)] // string (f) input not allowed to avoid path injection attacks [ProducesResponseType(404)] // not found - public async Task ListSizesByHash(string f) { // For serving jpeg files @@ -110,17 +119,25 @@ public async Task ListSizesByHash(string f) // Restrict the fileHash to letters and digits only // I/O function calls should not be vulnerable to path injection attacks if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) { return BadRequest(); } var data = new ThumbnailSizesExistStatusModel { - TinyMeta = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)), - Small = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)), - Large = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)), - ExtraLarge = _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(f, ThumbnailSize.ExtraLarge)) + TinyMeta = + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(f, ThumbnailSize.TinyMeta)), + Small = + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(f, ThumbnailSize.Small)), + Large = + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(f, ThumbnailSize.Large)), + ExtraLarge = + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(f, ThumbnailSize.ExtraLarge)) }; // Success has all items (except tinyMeta) @@ -137,11 +154,11 @@ public async Task ListSizesByHash(string f) return Json(data); case false when !string.IsNullOrEmpty(sourcePath): Response.StatusCode = 210; // A conflict, that the thumb is not generated yet - return Json("Thumbnail is not supported; for example you try to view a raw or video file"); + return Json( + "Thumbnail is not supported; for example you try to view a raw or video file"); default: return NotFound("not in index"); } - } private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize size) @@ -160,10 +177,11 @@ private IActionResult ReturnThumbnailResult(string f, bool json, ThumbnailSize s if ( json ) return Json("OK"); stream = _thumbnailStorage.ReadStream( - ThumbnailNameHelper.Combine(f, size)); + ThumbnailNameHelper.Combine(f, size)); // thumbs are always in jpeg - Response.Headers.Append("x-filename", new StringValues(FilenamesHelper.GetFileName(f + ".jpg"))); + Response.Headers.Append("x-filename", + new StringValues(FilenamesHelper.GetFileName(f + ".jpg"))); return File(stream, "image/jpeg"); } @@ -217,7 +235,7 @@ public async Task Thumbnail( // Restrict the fileHash to letters and digits only // I/O function calls should not be vulnerable to path injection attacks if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) { return BadRequest(); } @@ -248,7 +266,8 @@ public async Task Thumbnail( // remove from cache _query.ResetItemByHash(f); - if ( string.IsNullOrEmpty(filePath) || await _query.GetObjectByFilePathAsync(filePath) == null ) + if ( string.IsNullOrEmpty(filePath) || + await _query.GetObjectByFilePathAsync(filePath) == null ) { SetExpiresResponseHeadersToZero(); return NotFound("not in index"); @@ -260,7 +279,7 @@ public async Task Thumbnail( if ( !_iStorage.ExistFile(sourcePath) ) { return NotFound("There is no thumbnail image " + f + " and no source image " + - sourcePath); + sourcePath); } if ( !isSingleItem ) @@ -303,6 +322,7 @@ public async Task Thumbnail( [ProducesResponseType(400)] // string (f) input not allowed to avoid path injection attacks [ProducesResponseType(404)] // not found [ProducesResponseType(210)] // raw + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] public async Task ByZoomFactorAsync( string f, int z = 0, @@ -314,7 +334,7 @@ public async Task ByZoomFactorAsync( // Restrict the fileHash to letters and digits only // I/O function calls should not be vulnerable to path injection attacks if ( !Regex.IsMatch(f, "^[a-zA-Z0-9_-]+$", - RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) + RegexOptions.None, TimeSpan.FromMilliseconds(100)) ) { return BadRequest(); } @@ -327,6 +347,7 @@ public async Task ByZoomFactorAsync( { return NotFound("not in index"); } + sourcePath = filePath; } @@ -349,7 +370,8 @@ public async Task ByZoomFactorAsync( public void SetExpiresResponseHeadersToZero() { Request.HttpContext.Response.Headers.Remove("Cache-Control"); - Request.HttpContext.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate"); + Request.HttpContext.Response.Headers.Append("Cache-Control", + "no-cache, no-store, must-revalidate"); Request.HttpContext.Response.Headers.Remove("Pragma"); Request.HttpContext.Response.Headers.Append("Pragma", "no-cache"); diff --git a/starsky/starsky/Controllers/TrashController.cs b/starsky/starsky/Controllers/TrashController.cs index 555674fc3d..d4d7bc2bbc 100644 --- a/starsky/starsky/Controllers/TrashController.cs +++ b/starsky/starsky/Controllers/TrashController.cs @@ -20,21 +20,7 @@ public TrashController(IMoveToTrashService moveToTrashService) } /// - /// Is the system trash supported - /// - /// bool with json (IActionResult Result) - /// the item including the updated content - /// User unauthorized - [ProducesResponseType(typeof(bool), 200)] - [HttpGet("/api/trash/detect-to-use-system-trash")] - [Produces("application/json")] - public IActionResult DetectToUseSystemTrash() - { - return Json(_moveToTrashService.DetectToUseSystemTrash()); - } - - /// - /// (beta) Move a file to the trash + /// Move a file to the trash /// /// subPath filepath to file, split by dot comma (;) /// stack collections @@ -56,7 +42,8 @@ public async Task TrashMoveAsync(string f, bool collections = fal return BadRequest("No input files"); } - var fileIndexResultsList = await _moveToTrashService.MoveToTrashAsync(inputFilePaths.ToList(), collections); + var fileIndexResultsList = + await _moveToTrashService.MoveToTrashAsync(inputFilePaths.ToList(), collections); return Json(fileIndexResultsList); } diff --git a/starsky/starsky/Program.cs b/starsky/starsky/Program.cs index 98863a6f10..bf1c2ff7c3 100644 --- a/starsky/starsky/Program.cs +++ b/starsky/starsky/Program.cs @@ -5,17 +5,16 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; +using starsky.project.web.Helpers; namespace starsky { public static class Program { [SuppressMessage("Usage", "S6603: The collection-specific TrueForAll " + - "method should be used instead of the All extension")] + "method should be used instead of the All extension")] public static async Task Main(string[] args) { var appSettingsPath = Path.Join( @@ -29,8 +28,7 @@ public static async Task Main(string[] args) builder.Host.UseWindowsService(); var app = builder.Build(); - var hostLifetime = app.Services.GetRequiredService(); - startup.Configure(app, builder.Environment, hostLifetime); + startup.Configure(app, builder.Environment); await RunAsync(app, args.All(p => p != "--do-not-start")); } @@ -51,6 +49,7 @@ internal static async Task RunAsync(WebApplication webApplication, { return false; } + return true; } @@ -67,7 +66,7 @@ private static WebApplicationBuilder CreateWebHostBuilder(string[] args) builder.WebHost.ConfigureKestrel(k => { k.Limits.MaxRequestLineSize = 65536; //64Kb - // AddServerHeader removes the header: Server: Kestrel + // AddServerHeader removes the header: Server: Kestrel k.AddServerHeader = false; }); diff --git a/starsky/starsky/Properties/default-init-launchSettings.json b/starsky/starsky/Properties/default-init-launchSettings.json index 898032daae..36ab404143 100644 --- a/starsky/starsky/Properties/default-init-launchSettings.json +++ b/starsky/starsky/Properties/default-init-launchSettings.json @@ -25,6 +25,7 @@ "app__EnablePackageTelemetryDebug": "true", "app__DemoUnsafeDeleteStorageFolder": "false", "app__useSystemTrash": "true", + "app__UseLocalDesktop": "true", "app__accountRolesByEmailRegisterOverwrite__demo@qdraw.nl": "Administrator", "app__ThumbnailGenerationIntervalInMinutes": "15", "___app__OpenTelemetry__TracesEndpoint": "http://localhost:4318/v1/traces", diff --git a/starsky/starsky/Startup.cs b/starsky/starsky/Startup.cs index a7dc9f69cd..52eb3e16ae 100644 --- a/starsky/starsky/Startup.cs +++ b/starsky/starsky/Startup.cs @@ -57,7 +57,7 @@ public Startup(string[]? args = null) if ( !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("app__appsettingspath")) ) { Console.WriteLine("app__appSettingsPath: " + - Environment.GetEnvironmentVariable("app__appsettingspath")); + Environment.GetEnvironmentVariable("app__appsettingspath")); } _configuration = SetupAppSettings.AppSettingsToBuilder(args).ConfigureAwait(false) @@ -256,9 +256,7 @@ private static Func, Task> ReplaceR /// /// ApplicationBuilder /// Hosting Env - /// application Lifetime - public void Configure(IApplicationBuilder app, IHostEnvironment env, - IHostApplicationLifetime applicationLifetime) + public void Configure(IApplicationBuilder app, IHostEnvironment env) { app.UseResponseCompression(); @@ -289,7 +287,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env, app.UseAuthentication(); app.UseBasicAuthentication(); app.UseNoAccount(_appSettings?.NoAccountLocalhost == true || - _appSettings?.DemoUnsafeDeleteStorageFolder == true); + _appSettings?.DemoUnsafeDeleteStorageFolder == true); app.UseCheckIfAccountExist(); app.UseAuthorization(); @@ -322,7 +320,7 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env, internal (bool, bool, bool) SetupStaticFiles(IApplicationBuilder app, string assetsName = "assets") { - var result = (false, false, false); + var result = ( false, false, false ); // Allow Current Directory and wwwroot in Base Directory // AppSettings can be null when running tests @@ -351,19 +349,19 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env, // Check if clientapp is build and use the assets folder if ( !Directory.Exists(Path.Combine( - _appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) ) + _appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)) ) { return result; } app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = PrepareResponse, - FileProvider = new PhysicalFileProvider( + { + OnPrepareResponse = PrepareResponse, + FileProvider = new PhysicalFileProvider( Path.Combine(_appSettings.BaseDirectoryProject, "clientapp", "build", assetsName)), - RequestPath = $"/assets", - } + RequestPath = $"/assets", + } ); result.Item3 = true; return result; diff --git a/starsky/starsky/appsettings.json b/starsky/starsky/appsettings.json index c8358f69b0..d9ea300166 100644 --- a/starsky/starsky/appsettings.json +++ b/starsky/starsky/appsettings.json @@ -57,10 +57,14 @@ "SyncOnStartup": "true", "DemoUnsafeDeleteStorageFolder": "false", "useSystemTrash": "true", + "UseLocalDesktop": "false", "OpenTelemetry": { "TracesEndpoint": null, "MetricsEndpoint": null, - "LogsEndpoint": null + "LogsEndpoint": null, + "TracesHeader": null, + "MetricsHeader": null, + "LogsHeader": null }, "publishProfiles": { "_default": [ diff --git a/starsky/starsky/clientapp/.vscode/launch.json b/starsky/starsky/clientapp/.vscode/launch.json index c8df8623f5..0ed1589c17 100644 --- a/starsky/starsky/clientapp/.vscode/launch.json +++ b/starsky/starsky/clientapp/.vscode/launch.json @@ -5,16 +5,24 @@ "version": "0.2.0", "configurations": [ { - "type": "node", - "name": "vscode-jest-tests", + "name": "Jest file", + "type": "pwa-node", "request": "launch", - "args": ["test", "--runInBand"], - "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--runInBand", + "--watch", + "--coverage=false", + "--no-cache" + ], + "cwd": "${workspaceRoot}", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/react-scripts", - "protocol": "inspector" + "sourceMaps": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } } ] } diff --git a/starsky/starsky/clientapp/.vscode/settings.json b/starsky/starsky/clientapp/.vscode/settings.json index 9bb732eb5a..30dd5ec2f5 100644 --- a/starsky/starsky/clientapp/.vscode/settings.json +++ b/starsky/starsky/clientapp/.vscode/settings.json @@ -4,5 +4,8 @@ "eslint.alwaysShowStatus": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/starsky/starsky/clientapp/package.json b/starsky/starsky/clientapp/package.json index 45ea6fb6fa..d74b2c9bee 100644 --- a/starsky/starsky/clientapp/package.json +++ b/starsky/starsky/clientapp/package.json @@ -102,7 +102,7 @@ "projectRoot": "../../" } ], - "json", + "html", "cobertura" ], "coverageThreshold": { diff --git a/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx b/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx index 7c9722f343..247facd03d 100644 --- a/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/form-control/form-control.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { LimitLength } from "./limit-length"; @@ -27,11 +28,8 @@ const FormControl: React.FunctionComponent = ({ onBlur, ...pr // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageFieldMaxLength = language.token( - language.text( - "Het onderstaande veld mag maximaal {maxlength} tekens hebben", - "The field below can have a maximum of {maxlength} characters" - ), + const MessageFieldMaxLength = language.key( + localization.MessageFieldMaxLength, ["{maxlength}"], [maxlength.toString()] ); diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx index b6609b511d..a6aebc150c 100644 --- a/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/menu-option-modal/menu-option-modal.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from "@testing-library/react"; +import { LanguageLocalizationExample } from "../../../interfaces/ILanguageLocalization.ts"; import MenuOptionModal from "./menu-option-modal.tsx"; describe("MenuOption component", () => { @@ -7,7 +8,7 @@ describe("MenuOption component", () => { const setEnableMoreMenuMock = jest.fn(); render( { ); expect(screen.getByTestId("test")).toBeTruthy(); - expect(screen.getByTestId("test").innerHTML).toBe("Content"); + expect(screen.getByTestId("test").innerHTML).toBe(LanguageLocalizationExample.en); }); it("expect child no localisation field", () => { @@ -44,7 +45,7 @@ describe("MenuOption component", () => { const setEnableMoreMenuMock = jest.fn(); render( { const setEnableMoreMenuMock = jest.fn(); render( >; - localization?: { nl: string; en: string }; + localization?: ILanguageLocalization; setEnableMoreMenu?: React.Dispatch>; children?: React.ReactNode; } diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx index b4a5872ac6..e41bffc886 100644 --- a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.spec.tsx @@ -1,11 +1,12 @@ import { fireEvent, render, screen } from "@testing-library/react"; +import { LanguageLocalizationExample } from "../../../interfaces/ILanguageLocalization"; import MenuOption from "./menu-option"; describe("MenuOption component", () => { it("expect content", () => { render( {}} testName="test" isReadOnly={false} @@ -13,7 +14,7 @@ describe("MenuOption component", () => { ); expect(screen.getByTestId("test")).toBeTruthy(); - expect(screen.getByTestId("test").innerHTML).toBe("Content"); + expect(screen.getByTestId("test").innerHTML).toBe(LanguageLocalizationExample.en); }); it("expect child no localisation field", () => { @@ -30,7 +31,7 @@ describe("MenuOption component", () => { it("renders correctly with default props", () => { render( {}} testName="test-menu-option" isReadOnly={false} @@ -44,7 +45,7 @@ describe("MenuOption component", () => { it("renders correctly with custom props", () => { render( {}} testName="test-menu-option1" isReadOnly={false} @@ -59,7 +60,7 @@ describe("MenuOption component", () => { render( @@ -74,7 +75,7 @@ describe("MenuOption component", () => { render( diff --git a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx index d7621712c2..bd180bd4b7 100644 --- a/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/menu-option/menu-option.tsx @@ -1,12 +1,13 @@ import React, { memo } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import { ILanguageLocalization } from "../../../interfaces/ILanguageLocalization"; import { Language } from "../../../shared/language"; interface IMenuOptionProps { isReadOnly: boolean; testName: string; onClickKeydown: () => void; - localization?: { nl: string; en: string }; + localization?: ILanguageLocalization; children?: React.ReactNode; } diff --git a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx index 2abd358c76..bd115587d0 100644 --- a/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx +++ b/starsky/starsky/clientapp/src/components/atoms/more-menu/more-menu.tsx @@ -1,5 +1,6 @@ import React, { useEffect } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; type MoreMenuPropTypes = { @@ -17,7 +18,7 @@ const MoreMenu: React.FunctionComponent = ({ }) => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageMore = language.text("Meer", "More"); + const MessageMore = language.key(localization.MessageMore); const offMoreMenu = () => setEnableMoreMenu(false); @@ -44,7 +45,8 @@ const MoreMenu: React.FunctionComponent = ({ > {MessageMore} - + ); }; diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx index e310646146..bb4aec9542 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-pagination/archive-pagination.tsx @@ -2,6 +2,7 @@ import React, { memo } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { IRelativeObjects } from "../../../interfaces/IDetailView"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { UrlQuery } from "../../../shared/url-query"; import Link from "../../atoms/link/link"; @@ -17,8 +18,8 @@ const ArchivePagination: React.FunctionComponent = memo((props) = // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessagePrevious = language.text("Vorige", "Previous"); - const MessageNext = language.text("Volgende", "Next"); + const MessagePrevious = language.key(localization.MessagePrevious); + const MessageNext = language.key(localization.MessageNext); // used for reading current location const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx index 2251879c1f..59bb977abe 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-add-overwrite.tsx @@ -6,6 +6,7 @@ import useLocation from "../../../hooks/use-location/use-location"; import { PageType } from "../../../interfaces/IDetailView"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { ISidebarUpdate } from "../../../interfaces/ISidebarUpdate"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchPost from "../../../shared/fetch/fetch-post"; import { Keyboard } from "../../../shared/keyboard"; @@ -20,24 +21,14 @@ import Preloader from "../../atoms/preloader/preloader"; const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => { const settings = useGlobalSettings(); - const MessageAddName = new Language(settings.language).text("Toevoegen", "Add to"); - const MessageOverwriteName = new Language(settings.language).text("Overschrijven", "Overwrite"); - const MessageTitleName = new Language(settings.language).text("Titel", "Title"); - const MessageErrorReadOnly = new Language(settings.language).text( - "Eén of meerdere bestanden zijn alleen lezen. " + - "Alleen de bestanden met schrijfrechten zijn geupdate.", - "One or more files are read only. " + "Only the files with write permissions have been updated." - ); - const MessageErrorGenericFail = new Language(settings.language).text( - "Er is iets misgegaan met het updaten. Probeer het opnieuw", - "Something went wrong with the update. Please try again" - ); - - const MessageErrorNotFoundSourceMissing = new Language(settings.language).text( - "Eén of meerdere bestanden zijn al verdwenen. " + - "Alleen de bestanden die wel aanwezig zijn geupdate. Draai een handmatige sync", - "One or more files are already gone. " + - "Only the files that are present are updated. Run a manual sync" + const language = new Language(settings.language); + const MessageAddName = language.key(localization.MessageAddName); + const MessageOverwriteName = language.key(localization.MessageOverwriteName); + const MessageTitleName = language.key(localization.MessageTitleName); + const MessageWriteErrorReadOnly = language.key(localization.MessageWriteErrorReadOnly); + const MessageErrorGenericFail = language.key(localization.MessageErrorGenericFail); + const MessageErrorNotFoundSourceMissingRunSync = language.key( + localization.MessageErrorNotFoundSourceMissingRunSync ); const history = useLocation(); @@ -107,9 +98,9 @@ const ArchiveSidebarLabelEditAddOverwrite: React.FunctionComponent = () => { .then((anyData) => { const result = new CastToInterface().InfoFileIndexArray(anyData.data); result.forEach((element) => { - if (element.status === IExifStatus.ReadOnly) setIsError(MessageErrorReadOnly); + if (element.status === IExifStatus.ReadOnly) setIsError(MessageWriteErrorReadOnly); if (element.status === IExifStatus.NotFoundSourceMissing) - setIsError(MessageErrorNotFoundSourceMissing); + setIsError(MessageErrorNotFoundSourceMissingRunSync); if (element.status === IExifStatus.Ok || element.status === IExifStatus.Deleted) { dispatch({ type: "update", diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx index 161ce16fb9..18708c71c7 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit-search-replace.tsx @@ -5,6 +5,7 @@ import useLocation from "../../../hooks/use-location/use-location"; import { PageType } from "../../../interfaces/IDetailView"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { ISidebarUpdate } from "../../../interfaces/ISidebarUpdate"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchPost from "../../../shared/fetch/fetch-post"; import { Language } from "../../../shared/language"; @@ -19,22 +20,14 @@ import Preloader from "../../atoms/preloader/preloader"; const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageSearchAndReplaceName = language.text("Zoeken en vervangen", "Search and replace"); - const MessageTitleName = language.text("Titel", "Title"); - const MessageErrorReadOnly = new Language(settings.language).text( - "Eén of meerdere bestanden zijn alleen lezen. " + - "Alleen de bestanden met schrijfrechten zijn geupdate.", - "One or more files are read only. " + "Only the files with write permissions have been updated." + const MessageSearchAndReplaceNameLong = language.key( + localization.MessageSearchAndReplaceNameLong ); - const MessageErrorNotFoundSourceMissing = new Language(settings.language).text( - "Eén of meerdere bestanden zijn al verdwenen. " + - "Alleen de bestanden die wel aanwezig zijn geupdate. Draai een handmatige sync", - "One or more files are already gone. " + - "Only the files that are present are updated. Run a manual sync" - ); - const MessageErrorGenericFail = new Language(settings.language).text( - "Er is iets misgegaan met het updaten. Probeer het opnieuw", - "Something went wrong with the update. Please try again" + const MessageTitleName = language.key(localization.MessageTitleName); + const MessageWriteErrorReadOnly = language.key(localization.MessageWriteErrorReadOnly); + const MessageErrorGenericFail = language.key(localization.MessageErrorGenericFail); + const MessageErrorNotFoundSourceMissingRunSync = language.key( + localization.MessageErrorNotFoundSourceMissingRunSync ); const history = useLocation(); @@ -94,9 +87,9 @@ const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => { function handleFetchPostResponse(anyData: any) { const result = new CastToInterface().InfoFileIndexArray(anyData.data); result.forEach((element) => { - if (element.status === IExifStatus.ReadOnly) setIsError(MessageErrorReadOnly); + if (element.status === IExifStatus.ReadOnly) setIsError(MessageWriteErrorReadOnly); if (element.status === IExifStatus.NotFoundSourceMissing) - setIsError(MessageErrorNotFoundSourceMissing); + setIsError(MessageErrorNotFoundSourceMissingRunSync); if (element.status === IExifStatus.Ok || element.status === IExifStatus.Deleted) { dispatch({ type: "update", @@ -238,11 +231,11 @@ const ArchiveSidebarLabelEditSearchReplace: React.FunctionComponent = () => { data-test="replace-button" onClick={() => pushSearchAndReplace()} > - {MessageSearchAndReplaceName} + {MessageSearchAndReplaceNameLong} ) : ( )} diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx index c24f5032f3..177532dea4 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-label-edit.tsx @@ -1,6 +1,7 @@ import { useContext, useState } from "react"; import { ArchiveContext } from "../../../contexts/archive-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import { Language } from "../../../shared/language"; import SwitchButton from "../../atoms/switch-button/switch-button"; @@ -10,8 +11,10 @@ import ArchiveSidebarLabelEditSearchReplace from "./archive-sidebar-label-edit-s const ArchiveSidebarLabelEdit: React.FunctionComponent = () => { // Content const settings = useGlobalSettings(); - const MessageModifyName = new Language(settings.language).text("Wijzigen", "Modify"); - const MessageSearchAndReplaceName = new Language(settings.language).text("Vervangen", "Replace"); + const MessageModifyName = new Language(settings.language).key(localization.MessageModifyName); + const MessageSearchAndReplaceName = new Language(settings.language).key( + localization.MessageSearchAndReplaceNameShort + ); // Toggle const [replaceMode, setReplaceMode] = useState(false); diff --git a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx index 5ba0e6219a..297af8a6d7 100644 --- a/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/archive-sidebar/archive-sidebar-selection-list.tsx @@ -3,6 +3,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { Select } from "../../../shared/select"; import { URLPath } from "../../../shared/url-path"; @@ -16,8 +17,8 @@ const ArchiveSidebarSelectionList: React.FunctionComponent setIsDone(""); }, [props.filePath]); - const MessageColorClassIsUpdated = new Language(settings.language).text( - "Colorclass is bijgewerkt", - "Colorclass is updated" + const MessageColorClassIsUpdated = new Language(settings.language).key( + localization.MessageColorClassIsUpdated ); useKeyboardEvent(/[0-8]/, (event: KeyboardEvent) => { diff --git a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx index 3f1cb65539..a35d65da5a 100644 --- a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-select.tsx @@ -1,6 +1,7 @@ import "core-js/modules/es.array.find"; import React, { useEffect, useState } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import Notification, { NotificationType } from "../../atoms/notification/notification"; import Portal from "../../atoms/portal/portal"; @@ -25,15 +26,15 @@ const ColorClassSelect: React.FunctionComponent = (props const language = new Language(settings.language); const colorContent: Array = [ - language.text("Kleurloos", "Colorless"), - language.text("Roze", "Pink"), - language.text("Rood", "Red"), - language.text("Oranje", "Orange"), - language.text("Geel", "Yellow"), - language.text("Groen", "Green"), - language.text("Azuur", "Azure"), - language.text("Blauw", "Blue"), - language.text("Grijs", "Grey") + language.key(localization.ColorClassColour0), + language.key(localization.ColorClassColour1), + language.key(localization.ColorClassColour2), + language.key(localization.ColorClassColour3), + language.key(localization.ColorClassColour4), + language.key(localization.ColorClassColour5), + language.key(localization.ColorClassColour6), + language.key(localization.ColorClassColour7), + language.key(localization.ColorClassColour8) ]; const [currentColorClass, setCurrentColorClass] = React.useState(props.currentColorClass); diff --git a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts index 0881cc4b3b..deea83a8bb 100644 --- a/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts +++ b/starsky/starsky/clientapp/src/components/molecules/color-class-select/color-class-update-single.ts @@ -1,5 +1,6 @@ import { IGlobalSettings } from "../../../hooks/use-global-settings"; import { IExifStatus } from "../../../interfaces/IExifStatus"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchPost from "../../../shared/fetch/fetch-post"; import { Language, SupportedLanguages } from "../../../shared/language"; @@ -38,13 +39,8 @@ export class ColorClassUpdateSingle { this.clearAfter = clearAfter; } - private getMessageErrorReadOnly() { - return new Language(this.language).text( - "Eén of meerdere bestanden zijn alleen lezen. " + - "Alleen de bestanden met schrijfrechten zijn geupdate.", - "One or more files are read only. " + - "Only the files with write permissions have been updated." - ); + private getMessageWriteErrorReadOnly() { + return new Language(this.language).key(localization.MessageWriteErrorReadOnly); } public Update(colorClass: number) { @@ -67,7 +63,7 @@ export class ColorClassUpdateSingle { return item.status === IExifStatus.ReadOnly; }) ) { - this.setIsError(this.getMessageErrorReadOnly()); + this.setIsError(this.getMessageWriteErrorReadOnly()); return; } this.setCurrentColorClass(colorClass); diff --git a/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx b/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx index e03eba18f6..cb360e0a1b 100644 --- a/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/force-sync-wait-button/force-sync-wait-button.tsx @@ -3,6 +3,7 @@ import { ArchiveAction } from "../../../contexts/archive-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; import { IConnectionDefault } from "../../../interfaces/IConnectionDefault"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchGet from "../../../shared/fetch/fetch-get"; import FetchPost from "../../../shared/fetch/fetch-post"; @@ -64,10 +65,7 @@ const ForceSyncWaitButton: React.FunctionComponent const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageForceSync = language.text( - "Handmatig synchroniseren van huidige map", - "Synchronize current directory manually" - ); + const MessageForceSyncCurrentFolder = language.key(localization.MessageForceSyncCurrentFolder); const [startCounter, setStartCounter] = useState(0); // preloading icon @@ -101,7 +99,7 @@ const ForceSyncWaitButton: React.FunctionComponent <> {isLoading ? : ""} ); diff --git a/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx b/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx index ae333a17c6..d3f7218264 100644 --- a/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/health-check-for-updates/health-check-for-updates.tsx @@ -1,5 +1,6 @@ import useFetch from "../../../hooks/use-fetch"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { BrowserDetect } from "../../../shared/browser-detect"; import { DifferenceInDate } from "../../../shared/date"; import { Language } from "../../../shared/language"; @@ -35,24 +36,15 @@ const HealthCheckForUpdates: React.FunctionComponent = () => { const language = new Language(settings.language); - const ReleasesUrlToken = - " {releasesToken}"; - let WhereToFindRelease = language.token( - ReleasesUrlToken, + let WhereToFindRelease = language.key( + localization.MessageWhereToFindReleaseReleasesUrlTokenHtml, ["{releasesToken}"], - [language.text("Ga naar het release overzicht", "Go to the release overview")] + [language.key(localization.MessageWhereToFindReleaseReleasesUrlTokenContent)] ); if (new BrowserDetect().IsElectronApp()) - WhereToFindRelease = language.text( - "Ga naar het Help menu en dan release overzicht", - "Go to the release overview" - ); + WhereToFindRelease = language.key(localization.WhereToFindReleaseElectronApp); - const MessageNewVersionUpdateToken = language.text( - "Er is een nieuwe versie beschikbaar {WhereToFindRelease}", - "A new version is available {WhereToFindRelease}" - ); + const MessageNewVersionUpdateToken = language.key(localization.MessageNewVersionUpdateToken); const MessageNewVersionUpdateHtml = language.token( MessageNewVersionUpdateToken, diff --git a/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx b/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx index 7449cea9bf..bb2e197cda 100644 --- a/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/health-status-error/health-status-error.tsx @@ -1,6 +1,7 @@ import useFetch from "../../../hooks/use-fetch"; import useGlobalSettings from "../../../hooks/use-global-settings"; import { IHealthEntry } from "../../../interfaces/IHealthEntry"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { UrlQuery } from "../../../shared/url-query"; import Notification, { NotificationType } from "../../atoms/notification/notification"; @@ -9,9 +10,8 @@ const HealthStatusError: React.FunctionComponent = () => { const healthCheck = useFetch(new UrlQuery().UrlHealthDetails(), "get"); const settings = useGlobalSettings(); - const MessageCriticalErrors = new Language(settings.language).text( - "Er zijn kritieke fouten in de volgende onderdelen:", - "There are critical errors in the following components:" + const MessageHealthStatusCriticalErrors = new Language(settings.language).key( + localization.MessageHealthStatusCriticalErrorsWithTheFollowingComponents ); if ( @@ -21,7 +21,9 @@ const HealthStatusError: React.FunctionComponent = () => { ) return null; - const content: React.JSX.Element[] = [{MessageCriticalErrors}]; + const content: React.JSX.Element[] = [ + {MessageHealthStatusCriticalErrors} + ]; if (!healthCheck.data?.entries) { content.push( diff --git a/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx b/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx index 9f6d50b93c..442b209c1e 100644 --- a/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/item-text-list-view/item-text-list-view.tsx @@ -1,6 +1,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; interface ItemListProps { @@ -27,7 +28,7 @@ const ItemTextListView: React.FunctionComponent = (props) => { // Content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageNoPhotos = language.text("Er zijn geen foto's", "There are no pictures"); + const MessageNoPhotos = language.key(localization.MessageNoPhotos); if (!props.fileIndexItems) return ( diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx index 305d7b0723..3d42704dcd 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.spec.tsx @@ -69,7 +69,7 @@ describe("inline-search-suggest", () => { expect(queryByText("Trash")).toBeNull(); }); - it("hides logout menu item when useLocalDesktopUi is enabled", () => { + it("hides logout menu item when useLocalDesktop is enabled", () => { const props2 = { suggest: [], setFormFocus: jest.fn(), @@ -79,7 +79,7 @@ describe("inline-search-suggest", () => { } as any }, featuresResult: { - data: { useLocalDesktopUi: true }, + data: { useLocalDesktop: true }, statusCode: 200 } as IConnectionDefault, defaultText: "default text", diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx index eecc32f94e..bc46cf9607 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/internal/inline-search-suggest.tsx @@ -24,12 +24,12 @@ const InlineSearchSuggest: React.FunctionComponent = useEffect(() => { const dataFeatures = props.featuresResult?.data as IEnvFeatures | undefined; - if (dataFeatures?.systemTrashEnabled || dataFeatures?.useLocalDesktopUi) { + if (dataFeatures?.systemTrashEnabled || dataFeatures?.useLocalDesktop) { let newMenu = [...defaultMenu]; if (dataFeatures?.systemTrashEnabled) { newMenu = newMenu.filter((item) => item.key !== "trash"); } - if (dataFeatures?.useLocalDesktopUi) { + if (dataFeatures?.useLocalDesktop) { newMenu = newMenu.filter((item) => item.key !== "logout"); } setDefaultMenu([...newMenu]); @@ -63,6 +63,7 @@ const InlineSearchSuggest: React.FunctionComponent = name: language.key(localization.MessagePreferences), url: new UrlQuery().UrlPreferencesPage(), key: "preferences" + // command + shift + k -> see GlobalShortcuts }, { name: language.key(localization.MessageLogout), diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx index 7b8b724c1b..9c24da2e9e 100644 --- a/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/menu-inline-search/menu-inline-search.spec.tsx @@ -103,12 +103,12 @@ describe("menu-inline-search", () => { statusCode: 200, data: { systemTrashEnabled: true, - useLocalDesktopUi: false + useLocalDesktop: false } as IEnvFeatures } as IConnectionDefault; it("default menu should show logout and trash in default mode", () => { - dataFeaturesExample.data.useLocalDesktopUi = false; + dataFeaturesExample.data.useLocalDesktop = false; dataFeaturesExample.data.systemTrashEnabled = false; // usage ==> import * as useFetch from '../hooks/use-fetch'; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx new file mode 100644 index 0000000000..2f9293b4fe --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.spec.tsx @@ -0,0 +1,205 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import * as useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import * as useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; +import { IConnectionDefault } from "../../../interfaces/IConnectionDefault"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; +import * as Notification from "../../atoms/notification/notification"; +import MenuOptionDesktopEditorOpenSelectionNoSelectWarning from "./menu-option-desktop-editor-open-selection-no-select-warning"; + +describe("MenuOptionDesktopEditorOpenSelectionNoSelectWarning", () => { + it("should render without crashing", () => { + render(); + }); + + it("should show error notification when trying to open editor without selecting anything", () => { + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => { + return

{event.children}

; + }); + + render(); + + fireEvent.keyDown(window, { key: "e", ctrlKey: true }); + + waitFor(() => { + expect(notificationSpy).toHaveBeenCalledTimes(1); + + expect(screen.queryByTestId("notification-spy")).toBeTruthy(); + }); + }); + + it("should not show error notification when select is not empty", () => { + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => { + return

{event.children}

; + }); + + render( + + ); + fireEvent.keyDown(window, { key: "e", ctrlKey: true }); + expect(notificationSpy).toHaveBeenCalledTimes(0); + + expect(screen.queryByTestId("notification-spy")).toBeFalsy(); + }); + + it("should not show error notification when read-only mode is enabled", () => { + render(); + + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => { + return

{event.children}

; + }); + + fireEvent.keyDown(window, { key: "e", ctrlKey: true }); + expect(notificationSpy).toHaveBeenCalledTimes(0); + + expect(screen.queryByTestId("notification-spy")).toBeFalsy(); + }); + + it("should not show error notification when editor feature is disabled", () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: false + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => { + return

{event.children}

; + }); + + const component = render( + + ); + + fireEvent.keyDown(component.container, { key: "e", ctrlKey: true }); + + expect(screen.queryByTestId("notification-spy")).toBeFalsy(); + + expect(notificationSpy).toHaveBeenCalledTimes(0); + + expect(useFetchSpy).toHaveBeenCalled(); + }); + + it("give error message", () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => { + return

{event.children}

; + }); + + const useHotKeysSpy = jest + .spyOn(useHotKeys, "default") + .mockReset() + .mockImplementationOnce((event, callback) => { + if (event?.key == "e" && callback) { + callback({} as KeyboardEvent); + } + }); + + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageItemSelectionRequired = language.key(localization.MessageItemSelectionRequired); + + const component = render( + + ); + + fireEvent.keyDown(component.container, { key: "e", ctrlKey: true }); + + expect(screen.queryByTestId("notification-spy")).toBeTruthy(); + + const errorMessage = screen.queryByTestId("notification-spy")?.innerHTML; + + expect(errorMessage).toBe(MessageItemSelectionRequired); + + expect(screen.queryByTestId("notification-spy")?.innerHTML).toBeTruthy(); + + expect(notificationSpy).toHaveBeenCalledTimes(1); + expect(useHotKeysSpy).toHaveBeenCalledTimes(2); + + expect(useFetchSpy).toHaveBeenCalled(); + }); + + it("give error message and click away", () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const notificationSpy = jest + .spyOn(Notification, "default") + .mockReset() + .mockImplementationOnce((event) => { + return ( + <> +

{event.children}

+ + + ); + }); + + const useHotKeysSpy = jest + .spyOn(useHotKeys, "default") + .mockReset() + .mockImplementationOnce((event, callback) => { + if (event?.key == "e" && callback) { + callback({} as KeyboardEvent); + } + }); + + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageItemSelectionRequired = language.key(localization.MessageItemSelectionRequired); + + const component = render( + + ); + + fireEvent.keyDown(component.container, { key: "e", ctrlKey: true }); + + expect(screen.queryByTestId("notification-spy")).toBeTruthy(); + + const errorMessage = screen.queryByTestId("notification-spy")?.innerHTML; + + expect(errorMessage).toBe(MessageItemSelectionRequired); + + expect(screen.queryByTestId("notification-spy")?.innerHTML).toBeTruthy(); + + expect(notificationSpy).toHaveBeenCalledTimes(1); + expect(useHotKeysSpy).toHaveBeenCalledTimes(2); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("notification-spy-button")); + + const errorMessage2 = screen.queryByTestId("notification-spy")?.innerHTML; + + expect(errorMessage2).toBe(undefined); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.stories.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.stories.tsx new file mode 100644 index 0000000000..d54aa50494 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.stories.tsx @@ -0,0 +1,9 @@ +import MenuOptionDesktopEditorOpenSelectionNoSelectWarning from "./menu-option-desktop-editor-open-selection-no-select-warning"; + +export default { + title: "components/molecules/menu-option-desktop-editor-open-selection-no-select-warning" +}; + +export const Default = () => { + return ; +}; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.tsx new file mode 100644 index 0000000000..77939f5f84 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning.tsx @@ -0,0 +1,53 @@ +import { memo, useState } from "react"; +import useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import localization from "../../../localization/localization.json"; +import { Language } from "../../../shared/language"; +import { UrlQuery } from "../../../shared/url-query"; +import Notification, { NotificationType } from "../../atoms/notification/notification"; + +interface IMenuOptionDesktopEditorOpenSelectionNoSelectWarningProps { + select?: string[]; + isReadOnly: boolean; +} + +const MenuOptionDesktopEditorOpenSelectionNoSelectWarning: React.FunctionComponent = + memo(({ select, isReadOnly }) => { + const selectArray = select ?? []; + // Check API to know if feature is needed! + const featuresResult = useFetch(new UrlQuery().UrlApiFeaturesAppSettings(), "get"); + const dataFeatures = featuresResult?.data as IEnvFeatures | undefined; + + // for showing a notification + const [isError, setIsError] = useState(""); + + // Get language keys + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageItemSelectionRequired = language.key(localization.MessageItemSelectionRequired); + + /** + * Open editor with keys - command + e + */ + useHotKeys({ key: "e", ctrlKeyOrMetaKey: true }, () => { + if (dataFeatures?.openEditorEnabled !== true || isReadOnly || selectArray.length >= 1) { + setIsError(""); + return; + } + setIsError(MessageItemSelectionRequired); + }); + + return ( + <> + {isError !== "" ? ( + setIsError("")} type={NotificationType.default}> + {isError} + + ) : null} + + ); + }); + +export default MenuOptionDesktopEditorOpenSelectionNoSelectWarning; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.spec.tsx new file mode 100644 index 0000000000..0c2863b904 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.spec.tsx @@ -0,0 +1,565 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import * as useFetch from "../../../hooks/use-fetch"; +import * as useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; +import * as useLocation from "../../../hooks/use-location/use-location"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import { IConnectionDefault } from "../../../interfaces/IConnectionDefault"; +import { PageType, newIRelativeObjects } from "../../../interfaces/IDetailView"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import { IExifStatus } from "../../../interfaces/IExifStatus"; +import * as FetchPost from "../../../shared/fetch/fetch-post"; +import { UrlQuery } from "../../../shared/url-query"; +import * as Notification from "../../atoms/notification/notification"; +import * as ModalDesktopEditorOpenSelectionConfirmation from "../../organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation"; +import MenuOptionDesktopEditorOpenSelection, { + OpenDesktop, + StartMenuOptionDesktopEditorOpenSelection +} from "./menu-option-desktop-editor-open-selection"; + +describe("ModalDesktopEditorOpenConfirmation", () => { + const state = { + fileIndexItems: [ + { + filePath: "/file2.jpg", + fileName: "file1.jpg", + fileCollectionName: "file1", + fileHash: "1", + parentDirectory: "/", + status: IExifStatus.Ok + }, + { + filePath: "/file2.jpg", + fileName: "file2.jpg", + fileCollectionName: "file1", + fileHash: "1", + parentDirectory: "/", + status: IExifStatus.Ok + } + ], + relativeObjects: newIRelativeObjects(), + subPath: "", + breadcrumb: [], + colorClassUsage: [], + collectionsCount: 1, + colorClassActiveList: [], + pageType: PageType.Archive, + isReadOnly: false, + dateCache: 1 + } as IArchiveProps; + + describe("default function", () => { + it("renders without crashing", () => { + render( + {}} + /> + ); + }); + + it("-- calls StartMenuOptionDesktopEditorOpenSelection on hotkey trigger", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: false + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const useHotkeysSpy = jest.spyOn(useHotKeys, "default").mockImplementationOnce(() => { + return { key: "e", ctrlKey: true }; + }); + + const component = render( + {}} + /> + ); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useHotkeysSpy).toHaveBeenCalled(); + + component.unmount(); + }); + + it("calls StartMenuOptionDesktopEditorOpenSelection on hotkey trigger", async () => { + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const component = render( + {}} + /> + ); + fireEvent.keyDown(document.body, { key: "e", ctrlKey: true }); + + await waitFor(() => { + expect(useFetchSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(2); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(), + "f=%2Ffile1.jpg%3B%2Ffile2.jpg" + ); + + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 2, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ffile1.jpg%3B%2Ffile2.jpg&collections=true" + ); + component.unmount(); + }); + }); + + it("ModalDesktopEditorOpenSelectionConfirmation and open modal due FetchPost false", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: false, + statusCode: 200 + } as IConnectionDefault); + + const modalSpy = jest + .spyOn(ModalDesktopEditorOpenSelectionConfirmation, "default") + .mockImplementationOnce(() => <>); + + const useLocationFunction = () => { + return { + location: { + search: "?f=test1.jpg" + } as unknown as Location, + navigate: jest.fn() + }; + }; + + jest + .spyOn(useLocation, "default") + .mockImplementationOnce(useLocationFunction) + .mockImplementationOnce(useLocationFunction); + + jest.spyOn(Notification, "default").mockImplementationOnce(() => <>); + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(1); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(), + "f=%2Ffile1.jpg" + ); + + await waitFor(() => { + expect(modalSpy).toHaveBeenCalled(); + + component.unmount(); + }); + }); + + it("ModalDesktopEditorOpenSelectionConfirmation and FetchPost SearchPage is collections false", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const useLocationFunction = () => { + return { + location: { + search: "?f=test1.jpg&collections=true" // due search is false + } as unknown as Location, + navigate: jest.fn() + }; + }; + + jest + .spyOn(useLocation, "default") + .mockImplementationOnce(useLocationFunction) + .mockImplementationOnce(useLocationFunction); + + jest.spyOn(Notification, "default").mockImplementationOnce(() => <>); + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(1); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(), + "f=%2Ffile1.jpg" + ); + + await waitFor(() => { + expect(fetchPostSpy).toHaveBeenCalledTimes(2); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 2, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ffile1.jpg&collections=false" + ); + component.unmount(); + }); + }); + + it("ModalDesktopEditorOpenSelectionConfirmation and FetchPost error status Notification", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const mockIConnectionDefaultErrorStatus: Promise = Promise.resolve({ + data: null, // FAIL:! + statusCode: 500 + } as IConnectionDefault); + + const useLocationFunction = () => { + return { + location: { + search: "?f=test1.jpg" + } as unknown as Location, + navigate: jest.fn() + }; + }; + + jest + .spyOn(useLocation, "default") + .mockImplementationOnce(useLocationFunction) + .mockImplementationOnce(useLocationFunction); + + const notificationSpy = jest + .spyOn(Notification, "default") + .mockImplementationOnce(() => <>); + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultErrorStatus); + + const component = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(1); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(), + "f=%2Ffile1.jpg" + ); + + await waitFor(() => { + expect(fetchPostSpy).toHaveBeenCalledTimes(2); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 2, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ffile1.jpg&collections=true" + ); + // Failure + expect(notificationSpy).toHaveBeenCalled(); + + component.unmount(); + }); + }); + + it("ModalDesktopEditorOpenSelectionConfirmation and close due FetchPost true", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const useLocationFunction = () => { + return { + location: window.location, + navigate: jest.fn() + }; + }; + + jest + .spyOn(useLocation, "default") + .mockImplementationOnce(useLocationFunction) + .mockImplementationOnce(useLocationFunction); + + jest.spyOn(Notification, "default").mockImplementationOnce(() => <>); + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(1); + expect(fetchPostSpy).toHaveBeenLastCalledWith( + new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(), + "f=%2Ffile1.jpg" + ); + + component.unmount(); + }); + }); + + describe("StartMenuOptionDesktopEditorOpenSelection", () => { + it("StartMenuOptionDesktopEditorOpenSelection emthy array returns false", async () => { + const result = await StartMenuOptionDesktopEditorOpenSelection( + [], + false, + state, + jest.fn(), + "", + jest.fn(), + false + ); + expect(result).toBeFalsy(); + }); + + it("readonly", async () => { + const result = await StartMenuOptionDesktopEditorOpenSelection( + ["test"], + false, + state, + jest.fn(), + "", + jest.fn(), + true + ); + expect(result).toBeFalsy(); + }); + + it("sets modal confirmation open files if openWithoutConformationResult is false", async () => { + const select = ["file1.jpg", "file2.jpg"]; + const collections = false; + + const setIsError = jest.fn(); + const messageDesktopEditorUnableToOpen = "[for example] Unable to open desktop editor"; + const setModalConfirmationOpenFiles = jest.fn(); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: false, + statusCode: 200 + } as IConnectionDefault); + + jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + await StartMenuOptionDesktopEditorOpenSelection( + select, + collections, + state, + setIsError, + messageDesktopEditorUnableToOpen, + setModalConfirmationOpenFiles, + false + ); + expect(setModalConfirmationOpenFiles).toHaveBeenCalledWith(true); + expect(setIsError).not.toHaveBeenCalled(); // Error should not be set in this case + }); + + it("calls openDesktop if openWithoutConformationResult is true and open succceed", async () => { + const select = ["file1.jpg", "file2.jpg"]; + const collections = false; + const setIsError = jest.fn(); + const messageDesktopEditorUnableToOpen = "[for example] Unable to open desktop editor"; + const setModalConfirmationOpenFiles = jest.fn(); + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + await StartMenuOptionDesktopEditorOpenSelection( + select, + collections, + state, + setIsError, + messageDesktopEditorUnableToOpen, + setModalConfirmationOpenFiles, + false + ); + expect(setModalConfirmationOpenFiles).not.toHaveBeenCalled(); // Modal confirmation should not be set in this case + expect(setIsError).not.toHaveBeenCalled(); // Error should not be set in this case + expect(fetchPostSpy).toHaveBeenCalledTimes(2); + expect(fetchPostSpy).toHaveBeenLastCalledWith( + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ffile1.jpg%3B%2Ffile2.jpg&collections=false" + ); + }); + + it("calls openDesktop if openWithoutConformationResult is true but open failed", async () => { + const select = ["file1.jpg", "file2.jpg"]; + const collections = false; + const setIsError = jest.fn(); + const messageDesktopEditorUnableToOpen = "[for example] Unable to open desktop editor"; + const setModalConfirmationOpenFiles = jest.fn(); + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const mockIConnectionDefaultFailed: Promise = Promise.resolve({ + data: true, + statusCode: 300 + } as IConnectionDefault); + + jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultFailed); + + await StartMenuOptionDesktopEditorOpenSelection( + select, + collections, + state, + setIsError, + messageDesktopEditorUnableToOpen, + setModalConfirmationOpenFiles, + false + ); + expect(setModalConfirmationOpenFiles).not.toHaveBeenCalled(); // Modal confirmation should not be set in this case + expect(setIsError).toHaveBeenCalled(); + }); + }); + + describe("OpenDesktop", () => { + it("open Desktop emthy array returns false", async () => { + const result = await OpenDesktop([], false, state, jest.fn(), ""); + expect(result).toBeFalsy(); + }); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.stories.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.stories.tsx new file mode 100644 index 0000000000..ae6c9b6e09 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.stories.tsx @@ -0,0 +1,76 @@ +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import MoreMenu from "../../atoms/more-menu/more-menu"; +import MenuOptionDesktopEditorOpenSelection from "./menu-option-desktop-editor-open-selection"; + +export default { + title: "components/molecules/menu-option-desktop-editor-open-selection" +}; + +export const Default = () => { + return ( + {}}> + + + ); +}; + +Default.storyName = "default (no dialog)"; + +export const WithDialog = () => { + return ( + {}}> + + + ); +}; + +WithDialog.storyName = "with dialog"; + +export const ReadOnly = () => { + return ( + {}}> + + + ); +}; + +ReadOnly.storyName = "ReadOnly"; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.tsx new file mode 100644 index 0000000000..01c3b29891 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection.tsx @@ -0,0 +1,167 @@ +import React, { memo, useState } from "react"; +import useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; +import useLocation from "../../../hooks/use-location/use-location"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import { PageType } from "../../../interfaces/IDetailView"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import localization from "../../../localization/localization.json"; +import FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { URLPath } from "../../../shared/url-path"; +import { UrlQuery } from "../../../shared/url-query"; +import MenuOption from "../../atoms/menu-option/menu-option"; +import Notification, { NotificationType } from "../../atoms/notification/notification"; +import ModalDesktopEditorOpenSelectionConfirmation from "../../organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation"; + +interface IMenuOptionDesktopEditorOpenProps { + state: IArchiveProps; + select: string[]; + isReadOnly: boolean; + setEnableMoreMenu?: React.Dispatch>; +} + +export async function OpenDesktop( + select: string[], + collections: boolean, + state: IArchiveProps, + setIsError: React.Dispatch>, + messageDesktopEditorUnableToOpen: string +) { + const toDesktopOpenList = new URLPath().MergeSelectFileIndexItem(select, state.fileIndexItems); + if (!toDesktopOpenList || toDesktopOpenList.length === 0) return; + const selectParams = new URLPath().ArrayToCommaSeparatedStringOneParent(toDesktopOpenList, ""); + const urlOpen = new UrlQuery().UrlApiDesktopEditorOpen(); + + const bodyParams = new URLSearchParams(); + bodyParams.append("f", selectParams); + bodyParams.append("collections", collections.toString()); + + const openDesktopResult = await FetchPost(urlOpen, bodyParams.toString()); + if (openDesktopResult.statusCode >= 300) { + setIsError(messageDesktopEditorUnableToOpen); + } +} + +export async function StartMenuOptionDesktopEditorOpenSelection( + select: string[], + collections: boolean, + state: IArchiveProps, + setIsError: React.Dispatch>, + messageDesktopEditorUnableToOpen: string, + setModalConfirmationOpenFiles: (value: React.SetStateAction) => void, + isReadOnly: boolean +) { + if (isReadOnly) { + return; + } + const toDesktopOpenList = new URLPath().MergeSelectFileIndexItem(select, state.fileIndexItems); + if (!toDesktopOpenList || toDesktopOpenList.length === 0) return; + const selectParams = new URLPath().ArrayToCommaSeparatedStringOneParent(toDesktopOpenList, ""); + const urlCheck = new UrlQuery().UrlApiDesktopEditorOpenAmountConfirmationChecker(); + + const bodyParams = new URLSearchParams(); + bodyParams.append("f", selectParams); + + const openWithoutConformationResult = (await FetchPost(urlCheck, bodyParams.toString())).data; + if (openWithoutConformationResult === false) { + setModalConfirmationOpenFiles(true); + return; + } + await OpenDesktop(select, collections, state, setIsError, messageDesktopEditorUnableToOpen); +} + +const MenuOptionDesktopEditorOpenSelection: React.FunctionComponent = + memo(({ state, select, isReadOnly }) => { + // Check API to know if feature is needed! + const featuresResult = useFetch(new UrlQuery().UrlApiFeaturesAppSettings(), "get"); + const dataFeatures = featuresResult?.data as IEnvFeatures | undefined; + const history = useLocation(); + + // Get language keys + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageDesktopEditorUnableToOpen = language.key( + localization.MessageDesktopEditorUnableToOpen + ); + + // for showing a notification + const [isError, setIsError] = useState(""); + + const [modalConfirmationOpenFiles, setModalConfirmationOpenFiles] = useState(false); + + const isCollections = + state.pageType !== PageType.Search + ? new URLPath().StringToIUrl(history.location.search).collections !== false + : false; + + /** + * Open editor with keys - command + e + */ + useHotKeys({ key: "e", ctrlKeyOrMetaKey: true }, () => { + const isReadOnlyOrDisabled = !dataFeatures?.openEditorEnabled || isReadOnly; + console.log(`is ReadOnly/ or disabled: ${isReadOnlyOrDisabled}`); + + StartMenuOptionDesktopEditorOpenSelection( + select, + isCollections, + state, + setIsError, + MessageDesktopEditorUnableToOpen, + setModalConfirmationOpenFiles, + isReadOnlyOrDisabled + ).then(() => { + // do nothing + }); + }); + + return ( + <> + {/* Modal confirmation for open many files at one */} + {modalConfirmationOpenFiles ? ( + { + setModalConfirmationOpenFiles(!modalConfirmationOpenFiles); + }} + select={select} + state={state} + isCollections={isCollections} + setIsLoading={() => {}} + isOpen={modalConfirmationOpenFiles} + /> + ) : null} + + {isError !== "" ? ( + setIsError("")} type={NotificationType.danger}> + {isError} + + ) : null} + + {select.length >= 1 && dataFeatures?.openEditorEnabled === true ? ( + + StartMenuOptionDesktopEditorOpenSelection( + select, + isCollections, + state, + setIsError, + MessageDesktopEditorUnableToOpen, + setModalConfirmationOpenFiles, + isReadOnly + ) + } + localization={ + select.length === 1 + ? localization.MessageDesktopEditorOpenSingleFile + : localization.MessageDesktopEditorOpenMultipleFiles + } + /> + ) : null} + + ); + }); + +export default MenuOptionDesktopEditorOpenSelection; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.spec.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.spec.tsx new file mode 100644 index 0000000000..5887f2578f --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.spec.tsx @@ -0,0 +1,293 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import * as useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IConnectionDefault } from "../../../interfaces/IConnectionDefault"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import localization from "../../../localization/localization.json"; +import * as FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { UrlQuery } from "../../../shared/url-query"; +import * as Notification from "../../atoms/notification/notification"; +import MenuOptionDesktopEditorOpenSingle, { + OpenDesktopSingle +} from "./menu-option-desktop-editor-open-single"; + +describe("MenuOptionDesktopEditorOpenSingle", () => { + it("should render without errors", () => { + render(); + // You can add more specific assertions about the rendered output if needed + }); + + it("should call OpenDesktopSingle when MenuOption is clicked", () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle) + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const subPath = "/test.jpg"; + const collections = true; + const isReadOnly = false; + + const container = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open-single")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ftest.jpg&collections=true" + ); + + container.unmount(); + }); + + it("calls StartMenuOptionDesktopEditorOpenSelection on hotkey trigger", async () => { + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + } as IConnectionDefault); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve) + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const container = render( + + ); + + fireEvent.keyDown(document.body, { key: "e", ctrlKey: true }); + + await waitFor(() => { + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenCalledTimes(1); + + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ftest.jpg&collections=false" + ); + container.unmount(); + }); + }); + + it("feature toggle disabled", () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: false + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const subPath = "/test.jpg"; + const collections = true; + const isReadOnly = false; + + const container = render( + + ); + + waitFor(() => { + expect(useFetchSpy).toHaveBeenCalled(); + + expect(screen.queryByTestId("menu-option-desktop-editor-open-single")).toBeFalsy(); + }); + + container.unmount(); + }); + + it("should hide feature toggle - set Error", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce(() => <>); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: null, + statusCode: 500 + } as IConnectionDefault); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const subPath = "/test.jpg"; + const collections = true; + const isReadOnly = false; + + // Get language keys + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageDesktopEditorUnableToOpen = language.key( + localization.MessageDesktopEditorUnableToOpen + ); + + const container = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open-single")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ftest.jpg&collections=true" + ); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + expect(notificationSpy).toHaveBeenCalledWith( + { + callback: expect.anything(), + children: MessageDesktopEditorUnableToOpen, + type: "danger" + }, + {} + ); + }); + container.unmount(); + }); + + it("should hide feature toggle - set Error - click close", async () => { + const mockGetIConnectionDefaultFeatureToggle = { + statusCode: 200, + data: { + openEditorEnabled: true + } as IEnvFeatures + } as IConnectionDefault; + + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultFeatureToggle); + + const notificationSpy = jest.spyOn(Notification, "default").mockImplementationOnce((event) => ( + + )); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: null, + statusCode: 500 + } as IConnectionDefault); + + const fetchPostSpy = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefaultResolve); + + const subPath = "/test.jpg"; + const collections = true; + const isReadOnly = false; + + // Get language keys + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageDesktopEditorUnableToOpen = language.key( + localization.MessageDesktopEditorUnableToOpen + ); + + const container = render( + + ); + + expect(useFetchSpy).toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId("menu-option-desktop-editor-open-single")); + + expect(fetchPostSpy).toHaveBeenCalled(); + expect(fetchPostSpy).toHaveBeenNthCalledWith( + 1, + new UrlQuery().UrlApiDesktopEditorOpen(), + "f=%2Ftest.jpg&collections=true" + ); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + expect(notificationSpy).toHaveBeenCalledWith( + { + callback: expect.anything(), + children: MessageDesktopEditorUnableToOpen, + type: "danger" + }, + {} + ); + + const errorMessage1 = screen.queryByTestId("notification-spy-button")?.innerHTML; + expect(errorMessage1).toBe(MessageDesktopEditorUnableToOpen); + + fireEvent.click(screen.getByTestId("notification-spy-button")); + + const errorMessage2 = screen.queryByTestId("notification-spy-button")?.innerHTML; + + expect(errorMessage2).toBe(undefined); + }); + container.unmount(); + }); + + it("OpenDesktopSingle readonly should skip", async () => { + const result = await OpenDesktopSingle("/", false, jest.fn(), "error", true); + expect(result).toBeFalsy(); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.stories.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.stories.tsx new file mode 100644 index 0000000000..91799cde67 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.stories.tsx @@ -0,0 +1,30 @@ +import MoreMenu from "../../atoms/more-menu/more-menu"; +import MenuOptionDesktopEditorOpenSingle from "./menu-option-desktop-editor-open-single"; + +export default { + title: "components/molecules/menu-option-desktop-editor-open-single" +}; + +export const Default = () => { + return ( + {}}> + + + ); +}; + +Default.storyName = "default (no dialog)"; + +export const ReadOnly = () => { + return ( + {}}> + + + ); +}; + +ReadOnly.storyName = "readonly"; diff --git a/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.tsx b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.tsx new file mode 100644 index 0000000000..4ea7288709 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/molecules/menu-option-desktop-editor-open-single/menu-option-desktop-editor-open-single.tsx @@ -0,0 +1,102 @@ +import React, { memo, useState } from "react"; +import useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; +import { IEnvFeatures } from "../../../interfaces/IEnvFeatures"; +import localization from "../../../localization/localization.json"; +import FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { UrlQuery } from "../../../shared/url-query"; +import MenuOption from "../../atoms/menu-option/menu-option"; +import Notification, { NotificationType } from "../../atoms/notification/notification"; + +interface IMenuOptionDesktopEditorOpenSingleProps { + subPath: string; + collections: boolean; + isReadOnly: boolean; + setEnableMoreMenu?: React.Dispatch>; +} + +export async function OpenDesktopSingle( + subPath: string, + collections: boolean, + setIsError: React.Dispatch>, + messageDesktopEditorUnableToOpen: string, + isReadOnly: boolean +): Promise { + if (isReadOnly) { + return false; + } + const urlOpen = new UrlQuery().UrlApiDesktopEditorOpen(); + + const bodyParams = new URLSearchParams(); + bodyParams.append("f", subPath); + bodyParams.append("collections", collections.toString()); + + const openDesktopResult = await FetchPost(urlOpen, bodyParams.toString()); + if (openDesktopResult.statusCode >= 300) { + setIsError(messageDesktopEditorUnableToOpen); + } + return true; +} + +const MenuOptionDesktopEditorOpenSingle: React.FunctionComponent = + memo(({ subPath, collections, isReadOnly }) => { + // Check API to know if feature is needed! + const featuresResult = useFetch(new UrlQuery().UrlApiFeaturesAppSettings(), "get"); + const dataFeatures = featuresResult?.data as IEnvFeatures | undefined; + + // Get language keys + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageDesktopEditorUnableToOpen = language.key( + localization.MessageDesktopEditorUnableToOpen + ); + + // for showing a notification + const [isError, setIsError] = useState(""); + + /** + * Open editor with keys + */ + useHotKeys({ key: "e", ctrlKeyOrMetaKey: true }, () => { + OpenDesktopSingle( + subPath, + collections, + setIsError, + MessageDesktopEditorUnableToOpen, + isReadOnly + ).then(() => { + // do nothing + }); + }); + + return ( + <> + {isError !== "" ? ( + setIsError("")} type={NotificationType.danger}> + {isError} + + ) : null} + + {dataFeatures?.openEditorEnabled === true ? ( + + OpenDesktopSingle( + subPath, + collections, + setIsError, + MessageDesktopEditorUnableToOpen, + isReadOnly + ) + } + localization={localization.MessageDesktopEditorOpenSingleFile} + /> + ) : null} + + ); + }); + +export default MenuOptionDesktopEditorOpenSingle; diff --git a/starsky/starsky/clientapp/src/components/molecules/modal-drop-area-files-added/modal-drop-area-files-added.tsx b/starsky/starsky/clientapp/src/components/molecules/modal-drop-area-files-added/modal-drop-area-files-added.tsx index 173edb3a7a..686122d828 100644 --- a/starsky/starsky/clientapp/src/components/molecules/modal-drop-area-files-added/modal-drop-area-files-added.tsx +++ b/starsky/starsky/clientapp/src/components/molecules/modal-drop-area-files-added/modal-drop-area-files-added.tsx @@ -1,5 +1,6 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import Modal from "../../atoms/modal/modal"; import ItemTextListView from "../../molecules/item-text-list-view/item-text-list-view"; @@ -12,10 +13,7 @@ interface IModalDropAreaFilesAddedProps { const ModalDropAreaFilesAdded: React.FunctionComponent = (props) => { const settings = useGlobalSettings(); - const MessageFilesAdded = new Language(settings.language).text( - "Deze bestanden zijn toegevoegd", - "These files have been added" - ); + const MessageFilesAdded = new Language(settings.language).key(localization.MessageFilesAdded); return ( = memo((props) => // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessagePrevious = language.text("Vorige", "Previous"); - const MessageNext = language.text("Volgende", "Next"); + const MessagePrevious = language.key(localization.MessagePrevious); + const MessageNext = language.key(localization.MessageNext); // used for reading current location const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/components/organisms/application-exception/application-exception.tsx b/starsky/starsky/clientapp/src/components/organisms/application-exception/application-exception.tsx index e44fa5c8cc..9163df282a 100644 --- a/starsky/starsky/clientapp/src/components/organisms/application-exception/application-exception.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/application-exception/application-exception.tsx @@ -1,19 +1,14 @@ import { FunctionComponent } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import MenuDefault from "../menu-default/menu-default"; const ApplicationException: FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageApplicationException = language.text( - "We hebben een op dit moment een verstoring op de applicatie", - "We have a disruption on the application right now" - ); - const MessageRefreshPageTryAgain = language.text( - "Herlaad de applicatie om het opnieuw te proberen", - "Please reload the application to try again" - ); + const MessageApplicationException = language.key(localization.MessageApplicationException); + const MessageRefreshPageTryAgain = language.key(localization.MessageRefreshPageTryAgain); return ( <> diff --git a/starsky/starsky/clientapp/src/components/organisms/archive-sidebar/archive-sidebar.tsx b/starsky/starsky/clientapp/src/components/organisms/archive-sidebar/archive-sidebar.tsx index 5d8cd3d7d8..866bec7b0e 100644 --- a/starsky/starsky/clientapp/src/components/organisms/archive-sidebar/archive-sidebar.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/archive-sidebar/archive-sidebar.tsx @@ -2,6 +2,7 @@ import React, { memo, useEffect, useLayoutEffect } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { PageType } from "../../../interfaces/IDetailView"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { URLPath } from "../../../shared/url-path"; import ArchiveSidebarColorClass from "../../molecules/archive-sidebar/archive-sidebar-color-class"; @@ -13,10 +14,11 @@ const ArchiveSidebar: React.FunctionComponent = memo((arch // Content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageSelectionName = language.text("Selectie", "Selection"); - const MessageReadOnlyFolder = language.text("Alleen lezen map", "Read only folder"); - const MessageUpdateLabels = language.text("Labels wijzigingen", "Update labels"); - const MessageColorClassification = language.text("Kleur-Classificatie", "Color Classification"); + + const MessageSelectionName = language.key(localization.MessageSelectionName); + const MessageReadOnlyFolder = language.key(localization.MessageReadOnlyFolder); + const MessageUpdateLabels = language.key(localization.MessageUpdateLabels); + const MessageColorClassification = language.key(localization.MessageColorClassification); // Update view based on url parameters const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx b/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx index 2227267a26..026a40aaa2 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detail-view-sidebar/detail-view-sidebar.tsx @@ -7,6 +7,7 @@ import useLocation from "../../../hooks/use-location/use-location"; import { IDetailView } from "../../../interfaces/IDetailView"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { AsciiNull } from "../../../shared/ascii-null"; import AspectRatio from "../../../shared/aspect-ratio"; import BytesFormat from "../../../shared/bytes-format"; @@ -41,25 +42,15 @@ const DetailViewSidebar: React.FunctionComponent = memo // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageTitleName = language.text("Titel", "Title"); - const MessageInfoName = "Info"; - const MessageColorClassification = language.text("Kleur-Classificatie", "Color Classification"); - const MessageDateTimeAgoEdited = language.text("geleden bewerkt", "ago edited"); - const MessageDateLessThan1Minute = language.text( - "minder dan één minuut", - "less than one minute" - ); - const MessageDateMinutes = language.text("minuten", "minutes"); - const MessageDateHour = language.text("uur", "hour"); - - const MessageCopiedLabels = language.text( - "De labels zijn gekopieerd", - "The labels have been copied" - ); - const MessagePasteLabels = language.text( - "De labels zijn overschreven", - "The labels have been overwritten" - ); + const MessageTitleName = language.key(localization.MessageTitleName); + const MessageInfoName = language.key(localization.MessageInfoName); + const MessageColorClassification = language.key(localization.MessageColorClassification); + const MessageDateTimeAgoEdited = language.key(localization.MessageDateTimeAgoEdited); + const MessageDateLessThan1Minute = language.key(localization.MessageDateLessThan1Minute); + const MessageDateMinutes = language.key(localization.MessageDateMinutes); + const MessageDateHour = language.key(localization.MessageDateHour); + const MessageCopiedLabels = language.key(localization.MessageCopiedLabels); + const MessagePasteLabels = language.key(localization.MessagePasteLabels); const history = useLocation(); @@ -340,6 +331,8 @@ const DetailViewSidebar: React.FunctionComponent = memo

{index === 1 ? <>In een collectie: : null} {index + 1} van {collections.length}. {item === fileIndexItem.filePath && + fileIndexItem.imageWidth !== undefined && + fileIndexItem.imageHeight !== undefined && fileIndexItem.imageWidth !== 0 && fileIndexItem.imageHeight !== 0 ? ( diff --git a/starsky/starsky/clientapp/src/components/organisms/detailview-info-datetime/detailview-info-datetime.tsx b/starsky/starsky/clientapp/src/components/organisms/detailview-info-datetime/detailview-info-datetime.tsx index 6ff8b355d4..4add557d31 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detailview-info-datetime/detailview-info-datetime.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detailview-info-datetime/detailview-info-datetime.tsx @@ -2,6 +2,7 @@ import React, { memo } from "react"; import { DetailViewAction } from "../../../contexts/detailview-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { isValidDate, parseDate, parseTime } from "../../../shared/date"; import { Language } from "../../../shared/language"; import ModalDatetime from "../modal-edit-date-time/modal-edit-datetime"; @@ -17,10 +18,9 @@ const DetailViewInfoDateTime: React.FunctionComponent { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageCreationDate = language.text("Aanmaakdatum", "Creation date"); - const MessageCreationDateUnknownTime = language.text( - "is op een onbekend moment", - "is at an unknown time" + const MessageCreationDate = language.key(localization.MessageCreationDate); + const MessageCreationDateIsAtUnknownTime = language.key( + localization.MessageCreationDateIsAtUnknownTime ); const [modalDatetimeOpen, setModalDatetimeOpen] = React.useState(false); @@ -70,7 +70,7 @@ const DetailViewInfoDateTime: React.FunctionComponent {MessageCreationDate} -

{MessageCreationDateUnknownTime}

+

{MessageCreationDateIsAtUnknownTime}

) : null} diff --git a/starsky/starsky/clientapp/src/components/organisms/detailview-info-location/detailview-info-location.tsx b/starsky/starsky/clientapp/src/components/organisms/detailview-info-location/detailview-info-location.tsx index b865dc7e15..763e1aefd9 100644 --- a/starsky/starsky/clientapp/src/components/organisms/detailview-info-location/detailview-info-location.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/detailview-info-location/detailview-info-location.tsx @@ -4,6 +4,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; import { IGeoLocationModel } from "../../../interfaces/IGeoLocationModel"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import ModalGeo from "../modal-geo/modal-geo"; @@ -33,10 +34,9 @@ const DetailViewInfoLocation: React.FunctionComponent { const component = render(); expect(useHotkeysSpy).toHaveBeenCalled(); - expect(useHotkeysSpy).toHaveBeenCalledTimes(1); + expect(useHotkeysSpy).toHaveBeenNthCalledWith( + 1, + { ctrlKeyOrMetaKey: true, key: "a" }, + expect.anything(), + [] + ); component.unmount(); }); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx index 186f17012f..9d2f1ee5bb 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-archive/menu-archive.tsx @@ -14,6 +14,8 @@ import HamburgerMenuToggle from "../../atoms/hamburger-menu-toggle/hamburger-men import MenuOptionModal from "../../atoms/menu-option-modal/menu-option-modal"; import MoreMenu from "../../atoms/more-menu/more-menu"; import MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; +import MenuOptionDesktopEditorOpenSelectionNoSelectWarning from "../../molecules/menu-option-desktop-editor-open-selection-no-select-warning/menu-option-desktop-editor-open-selection-no-select-warning"; +import MenuOptionDesktopEditorOpenSelection from "../../molecules/menu-option-desktop-editor-open-selection/menu-option-desktop-editor-open-selection"; import MenuOptionMoveFolderToTrash from "../../molecules/menu-option-move-folder-to-trash/menu-option-move-folder-to-trash"; import MenuOptionMoveToTrash from "../../molecules/menu-option-move-to-trash/menu-option-move-to-trash"; import { MenuOptionSelectionAll } from "../../molecules/menu-option-selection-all/menu-option-selection-all"; @@ -38,6 +40,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { // Content const MessageSelectAction = language.key(localization.MessageSelectAction); + const MessageLabels = language.key(localization.MessageLabels); const [hamburgerMenu, setHamburgerMenu] = React.useState(false); const [enableMoreMenu, setEnableMoreMenu] = React.useState(false); @@ -170,6 +173,11 @@ const MenuArchive: React.FunctionComponent = memo(() => { + + {!select ? ( ) : null} @@ -261,6 +269,7 @@ const MenuArchive: React.FunctionComponent = memo(() => { {/* onClick={() => allSelection()} */} + {select.length >= 1 ? ( <> = memo(() => { setSelect={setSelect} isReadOnly={readOnly} /> + + ) : null} + = memo(() => { localization={localization.MessageDisplayOptions} testName="display-options" /> + = memo(() => { set={setIsSynchronizeManuallyOpen} localization={localization.MessageSynchronizeManually} /> + {state ? ( = ({ state, d const MessageMoveToTrash = language.key(localization.MessageMoveToTrash); const MessageIncludingColonWord = language.key(localization.MessageIncludingColonWord); const MessageRestoreFromTrash = language.key(localization.MessageRestoreFromTrash); + const MessageLabels = language.key(localization.MessageLabels); const history = useLocation(); @@ -293,7 +295,7 @@ const MenuDetailView: React.FunctionComponent = ({ state, d event.key === "Enter" && toggleLabels(); }} > - Labels + {MessageLabels} @@ -359,6 +361,12 @@ const MenuDetailView: React.FunctionComponent = ({ state, d set={setIsModalPublishOpen} localization={localization.MessagePublish} /> + + diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx index 620d755efa..142389c781 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.spec.tsx @@ -3,6 +3,7 @@ import React from "react"; import { act } from "react-dom/test-utils"; import * as useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; import { IArchive } from "../../../interfaces/IArchive"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { Router } from "../../../router-app/router-app"; import * as MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; @@ -80,10 +81,17 @@ describe("MenuSearch", () => { .mockImplementationOnce(() => contextValues) .mockImplementationOnce(() => contextValues); - const component = render(); + const component = render( + + ); expect(useHotkeysSpy).toHaveBeenCalled(); - expect(useHotkeysSpy).toHaveBeenCalledTimes(1); + expect(useHotkeysSpy).toHaveBeenNthCalledWith( + 1, + { ctrlKeyOrMetaKey: true, key: "a" }, + expect.anything(), + [] + ); jest.spyOn(React, "useContext").mockRestore(); component.unmount(); diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx index cbe24f3a79..8e4007ac7a 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-search/menu-search.tsx @@ -37,7 +37,8 @@ const MenuSearch: React.FunctionComponent = ({ state, dispatch const language = new Language(settings.language); // Content - const MessageSelectAction = language.text("Selecteer", "Select"); + const MessageSelectAction = language.key(localization.MessageSelectAction); + const MessageLabels = language.key(localization.MessageLabels); // Selection const history = useLocation(); @@ -131,7 +132,7 @@ const MenuSearch: React.FunctionComponent = ({ state, dispatch event.key === "Enter" && toggleLabels(); }} > - Labels + {MessageLabels} ) : null} diff --git a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx index 74ecea8109..2f0911f477 100644 --- a/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/menu-trash/menu-trash.tsx @@ -4,6 +4,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import useHotKeys from "../../../hooks/use-keyboard/use-hotkeys"; import useLocation from "../../../hooks/use-location/use-location"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; import FetchPost from "../../../shared/fetch/fetch-post"; import { FileListCache } from "../../../shared/filelist-cache"; import { Language } from "../../../shared/language"; @@ -12,6 +13,8 @@ import { Select } from "../../../shared/select"; import { URLPath } from "../../../shared/url-path"; import { UrlQuery } from "../../../shared/url-query"; import HamburgerMenuToggle from "../../atoms/hamburger-menu-toggle/hamburger-menu-toggle"; +import MenuOptionModal from "../../atoms/menu-option-modal/menu-option-modal.tsx"; +import MenuOption from "../../atoms/menu-option/menu-option.tsx"; import MoreMenu from "../../atoms/more-menu/more-menu"; import Preloader from "../../atoms/preloader/preloader"; import MenuSearchBar from "../../molecules/menu-inline-search/menu-inline-search"; @@ -20,9 +23,6 @@ import { MenuOptionSelectionUndo } from "../../molecules/menu-option-selection-u import { MenuSelectCount } from "../../molecules/menu-select-count/menu-select-count"; import ModalForceDelete from "../modal-force-delete/modal-force-delete"; import NavContainer from "../nav-container/nav-container"; -import MenuOption from "../../atoms/menu-option/menu-option.tsx"; -import localization from "../../../localization/localization.json"; -import MenuOptionModal from "../../atoms/menu-option-modal/menu-option-modal.tsx"; interface IMenuTrashProps { state: IArchiveProps; @@ -34,7 +34,7 @@ const MenuTrash: React.FunctionComponent = ({ state, dispatch } const language = new Language(settings.language); // Content - const MessageSelectAction = language.text("Selecteer", "Select"); + const MessageSelectAction = language.key(localization.MessageSelectAction); const [hamburgerMenu, setHamburgerMenu] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false); diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-archive-mkdir/modal-archive-mkdir.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-archive-mkdir/modal-archive-mkdir.tsx index 8a012dbab7..708ca5f1ba 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-archive-mkdir/modal-archive-mkdir.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-archive-mkdir/modal-archive-mkdir.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ArchiveAction } from "../../../contexts/archive-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchGet from "../../../shared/fetch/fetch-get"; import FetchPost from "../../../shared/fetch/fetch-post"; @@ -27,19 +28,10 @@ const ModalArchiveMkdir: React.FunctionComponent = ({ // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageFeatureName = language.text("Nieuwe map aanmaken", "Create new folder"); - const MessageNonValidDirectoryName = language.text( - "Controleer de naam, deze map kan niet zo worden aangemaakt", - "Check the name, this folder cannot be created in this way" - ); - const MessageGeneralMkdirCreateError = language.text( - "Er is misgegaan met het aanmaken van deze map", - "An error occurred while creating this folder" - ); - const MessageDirectoryExistError = language.text( - "De map bestaat al, probeer een andere naam", - "The folder already exists, try a different name" - ); + const MessageCreateNewFolderFeature = language.key(localization.MessageCreateNewFolderFeature); + const MessageNonValidDirectoryName = language.key(localization.MessageNonValidDirectoryName); + const MessageGeneralMkdirCreateError = language.key(localization.MessageGeneralMkdirCreateError); + const MessageDirectoryExistError = language.key(localization.MessageDirectoryExistError); // to show errors const useErrorHandler = (initialState: string | null) => { @@ -124,7 +116,7 @@ const ModalArchiveMkdir: React.FunctionComponent = ({ }} >
-
{MessageFeatureName}
+
{MessageCreateNewFolderFeature}
= ({ data-test="modal-archive-mkdir-btn-default" onClick={pushRenameChange} > - {isLoading ? "Loading..." : MessageFeatureName} + {isLoading ? "Loading..." : MessageCreateNewFolderFeature}
diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-archive-rename/modal-archive-rename.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-archive-rename/modal-archive-rename.tsx index 8122916938..5478f36579 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-archive-rename/modal-archive-rename.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-archive-rename/modal-archive-rename.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ArchiveAction } from "../../../contexts/archive-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; +import localization from "../../../localization/localization.json"; import FetchPost from "../../../shared/fetch/fetch-post"; import { FileExtensions } from "../../../shared/file-extensions"; import { FileListCache } from "../../../shared/filelist-cache"; @@ -21,15 +22,11 @@ const ModalArchiveRename: React.FunctionComponent = (pr // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageRenameFolder = language.text("Huidige mapnaam wijzigen", "Rename current folder"); - const MessageNonValidDirectoryName: string = language.text( - "Deze mapnaam is niet valide", - "Directory name is not valid" - ); - const MessageGeneralError: string = language.text( - "Er is iets misgegaan met de aanvraag, probeer het later opnieuw", - "Something went wrong with the request, please try again later" + const MessageRenameFolder = language.key(localization.MessageRenameCurrentFolder); + const MessageNonValidDirectoryName: string = language.key( + localization.MessageNonValidDirectoryName ); + const MessageGeneralError: string = language.key(localization.MessageErrorGenericFail); // to show errors const useErrorHandler = (initialState: string | null) => { diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-archive-synchronize-manually/modal-archive-synchronize-manually.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-archive-synchronize-manually/modal-archive-synchronize-manually.tsx index 7af03621bc..6cc1a0d4ac 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-archive-synchronize-manually/modal-archive-synchronize-manually.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-archive-synchronize-manually/modal-archive-synchronize-manually.tsx @@ -4,6 +4,7 @@ import useGlobalSettings from "../../../hooks/use-global-settings"; import useInterval from "../../../hooks/use-interval"; import useLocation from "../../../hooks/use-location/use-location"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; import { CastToInterface } from "../../../shared/cast-to-interface"; import FetchGet from "../../../shared/fetch/fetch-get"; import FetchPost from "../../../shared/fetch/fetch-post"; @@ -27,32 +28,13 @@ const ModalArchiveSynchronizeManually: React.FunctionComponent { + afterEach(() => { + jest.restoreAllMocks(); + }); + + const exampleState = { + fileIndexItems: [ + { + fileName: "test.jpg", + parentDirectory: "/" + } + ] + } as IArchiveProps; + + it("renders correctly", () => { + const component = render( + {}} + state={exampleState} + setIsLoading={() => {}} + isCollections={false} + /> + ); + + expect(screen.getByTestId("editor-open-heading")).toBeTruthy(); + expect(screen.getByTestId("editor-open-text")).toBeTruthy(); + expect(screen.getByTestId("editor-open-confirmation-no")).toBeTruthy(); + expect(screen.getByTestId("editor-open-confirmation-yes")).toBeTruthy(); + + component.unmount(); + }); + + it("calls handleExit on cancel button click", () => { + const handleExit = jest.fn(); + const component = render( + {}} + isCollections={false} + /> + ); + + fireEvent.click(screen.getByTestId("editor-open-confirmation-no")); + expect(handleExit).toHaveBeenCalled(); + + component.unmount(); + }); + + it("calls handleExit on close left top button click", () => { + jest.spyOn(Modal, "default").mockImplementationOnce((element) => { + element.handleExit(); + }); + + // it auto close the modal + const handleExit = jest.fn(); + const component = render( + {}} + isCollections={false} + /> + ); + + expect(handleExit).toHaveBeenCalled(); + + component.unmount(); + }); + + it("calls OpenDesktop and fetchPost / handleExit on confirm button click", async () => { + const handleExit = jest.fn(); + const setIsLoading = jest.fn(); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: true, + statusCode: 200 + }); + const mockFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementation(() => mockIConnectionDefaultResolve) + .mockImplementation(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + fireEvent.click(screen.getByTestId("editor-open-confirmation-yes")); + + await waitFor(() => { + expect(mockFetchPost).toHaveBeenCalled(); + expect(handleExit).toHaveBeenCalled(); + + component.unmount(); + }); + }); + + it("calls OpenDesktop and handleExit / fetch Post on confirm button enter", async () => { + const handleExit = jest.fn(); + const setIsLoading = jest.fn(); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: false, + statusCode: 200 + }); + const mockFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementation(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + const keyDownEvent = createEvent.keyDown(screen.getByTestId("editor-open-confirmation-yes"), { + key: "Enter" + }); + + fireEvent(screen.getByTestId("editor-open-confirmation-yes"), keyDownEvent); + + await waitFor(() => { + expect(mockFetchPost).toHaveBeenCalled(); + expect(handleExit).toHaveBeenCalled(); + + component.unmount(); + }); + }); + + it("does nothing when pressing different key on confirmation-yes", async () => { + const handleExit = jest.fn(); + const setIsLoading = jest.fn(); + + const mockIConnectionDefaultResolve: Promise = Promise.resolve({ + data: false, + statusCode: 200 + }); + const mockFetchPost = jest + .spyOn(FetchPost, "default") + .mockImplementation(() => mockIConnectionDefaultResolve); + + const component = render( + + ); + + const keyDownEvent = createEvent.keyDown(screen.getByTestId("editor-open-confirmation-yes"), { + key: "q" + }); + + fireEvent(screen.getByTestId("editor-open-confirmation-yes"), keyDownEvent); + + await waitFor(() => { + expect(mockFetchPost).toHaveBeenCalledTimes(0); + expect(handleExit).toHaveBeenCalledTimes(0); + + component.unmount(); + }); + }); + + it("calls setIsError if FetchPost fails", async () => { + const mockIConnectionDefaultResolve = Promise.resolve({ + data: false, + statusCode: 400 + }); + + const mockFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementation(() => mockIConnectionDefaultResolve) + .mockImplementation(() => mockIConnectionDefaultResolve); + + const component = render( + {}} + state={exampleState} + setIsLoading={() => {}} + isCollections={false} + /> + ); + + fireEvent.click(screen.getByTestId("editor-open-confirmation-yes")); + + await waitFor(() => { + expect(mockFetchPost).toHaveBeenCalled(); + expect(screen.getByTestId("editor-open-error")).toBeTruthy(); + + component.unmount(); + }); + }); + + it("open Desktop emthy array returns false", async () => { + const result = await OpenDesktop([], false, exampleState, jest.fn(), ""); + expect(result).toBeFalsy(); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.stories.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.stories.tsx new file mode 100644 index 0000000000..d902a1aa0a --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.stories.tsx @@ -0,0 +1,30 @@ +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import ModalDesktopEditorOpenSelectionConfirmation from "./modal-desktop-editor-open-selection-confirmation"; + +export default { + title: "components/organisms/modal-desktop-editor-open-selection-confirmation" +}; + +export const Default = () => { + return ( + {}} + isOpen={true} + handleExit={() => {}} + /> + ); +}; + +Default.storyName = "default"; diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.tsx new file mode 100644 index 0000000000..48e85dcdea --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/modal-desktop-editor-open-selection-confirmation/modal-desktop-editor-open-selection-confirmation.tsx @@ -0,0 +1,137 @@ +import { useState } from "react"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; +import FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { URLPath } from "../../../shared/url-path"; +import { UrlQuery } from "../../../shared/url-query"; +import Modal from "../../atoms/modal/modal"; + +interface IModalDesktopEditorOpenConfirmationProps { + isOpen: boolean; + select: Array | undefined; + handleExit(): void; + state: IArchiveProps; + setIsLoading: React.Dispatch>; + isCollections: boolean; +} + +export async function OpenDesktop( + select: string[], + collections: boolean, + state: IArchiveProps, + setIsError: React.Dispatch>, + messageDesktopEditorUnableToOpen: string +): Promise { + const toDesktopOpenList = new URLPath().MergeSelectFileIndexItem(select, state.fileIndexItems); + if (!toDesktopOpenList || toDesktopOpenList.length == 0) return false; + const selectParams = new URLPath().ArrayToCommaSeparatedStringOneParent(toDesktopOpenList, ""); + const urlOpen = new UrlQuery().UrlApiDesktopEditorOpen(); + + const bodyParams = new URLSearchParams(); + bodyParams.append("f", selectParams); + bodyParams.append("collections", collections.toString()); + + const openDesktopResult = await FetchPost(urlOpen, bodyParams.toString()); + if (openDesktopResult.statusCode >= 300) { + setIsError(messageDesktopEditorUnableToOpen); + return false; + } + return true; +} + +const ModalDesktopEditorOpenSelectionConfirmation: React.FunctionComponent< + IModalDesktopEditorOpenConfirmationProps +> = ({ select, handleExit, isOpen, state, isCollections }) => { + // content + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageDesktopEditorConfirmationIntroText = language.key( + localization.MessageDesktopEditorConfirmationIntroText + ); + const MessageDesktopEditorConfirmationHeading = language.key( + localization.MessageDesktopEditorConfirmationHeading + ); + const MessageCancel = language.key(localization.MessageCancel); + const MessageDesktopEditorOpenMultipleFiles = language.key( + localization.MessageDesktopEditorOpenMultipleFiles + ); + const MessageDesktopEditorUnableToOpen = language.key( + localization.MessageDesktopEditorUnableToOpen + ); + + // for showing a notification + const [isError, setIsError] = useState(""); + + return ( + { + handleExit(); + }} + > + <> +
+ {MessageDesktopEditorConfirmationHeading} +
+
+

{MessageDesktopEditorConfirmationIntroText}

+ {isError ? ( + <> +
+
+ {isError} +
+ + ) : null} + + + +
+ +
+ ); +}; + +export default ModalDesktopEditorOpenSelectionConfirmation; diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-detailview-rename-file/modal-detailview-rename-file.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-detailview-rename-file/modal-detailview-rename-file.tsx index 98491538e6..3fb71f8be9 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-detailview-rename-file/modal-detailview-rename-file.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-detailview-rename-file/modal-detailview-rename-file.tsx @@ -4,6 +4,7 @@ import useLocation from "../../../hooks/use-location/use-location"; import { IDetailView, newDetailView } from "../../../interfaces/IDetailView"; import { IExifStatus } from "../../../interfaces/IExifStatus"; import { newIFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import FetchPost from "../../../shared/fetch/fetch-post"; import { FileExtensions } from "../../../shared/file-extensions"; import { FileListCache } from "../../../shared/filelist-cache"; @@ -25,19 +26,12 @@ const ModalDetailviewRenameFile: React.FunctionComponent // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageNonValidExtension: string = language.text( - "Dit bestand kan zo niet worden weggeschreven", - "This file cannot be saved" + const MessageNonValidExtension = language.key(localization.MessageNonValidExtension); + const MessageChangeToDifferentExtension = language.key( + localization.MessageChangeToDifferentExtension ); - const MessageChangeToDifferentExtension = language.text( - "Let op! Je veranderd de extensie van het bestand, " + "deze kan hierdoor onleesbaar worden", - "Pay attention! You change the file extension, which can make it unreadable" - ); - const MessageGeneralError: string = language.text( - "Er is iets misgegaan met de aanvraag, probeer het later opnieuw", - "Something went wrong with the request, please try again later" - ); - const MessageRenameFileName = language.text("Bestandsnaam wijzigen", "Rename file name"); + const MessageRenameServerError = language.key(localization.MessageRenameServerError); + const MessageRenameFileName = language.key(localization.MessageRenameFileName); // Fallback for no context if (!state) { @@ -130,7 +124,7 @@ const ModalDetailviewRenameFile: React.FunctionComponent const result = await FetchPost(new UrlQuery().UrlDiskRename(), bodyParams.toString()); if (result.statusCode !== 200) { - setError(MessageGeneralError); + setError(MessageRenameServerError); // and renewable setIsLoading(false); setIsFormEnabled(true); diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-display-options/modal-display-options.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-display-options/modal-display-options.tsx index 7f8fccda4f..91e73c301d 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-display-options/modal-display-options.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-display-options/modal-display-options.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { SortType } from "../../../interfaces/IArchive"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { URLPath } from "../../../shared/url-path"; import Modal from "../../atoms/modal/modal"; @@ -17,13 +18,25 @@ const ModalDisplayOptions: React.FunctionComponent = // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageDisplayOptions = language.text("Weergave opties", "Display options"); - const MessageSwitchButtonCollectionsOff = language.text("Toon raw bestanden", "Show raw files"); - const MessageSwitchButtonCollectionsOn = language.text("Verberg raw bestanden", "Hide Raw files"); - const MessageSwitchButtonIsSingleItemOff = language.text("Alles inladen", "Load everything"); - const MessageSwitchButtonIsSingleItemOn = language.text("Klein inladen", "Small loading"); - const MessageSwitchButtonIsSocketOn = language.text("Realtime updates", "Realtime updates"); - const MessageSwitchButtonIsSocketOff = language.text("Ververs zelf", "Refresh yourself"); + const MessageDisplayOptions = language.key(localization.MessageDisplayOptions); + const MessageDisplayOptionsSwitchButtonCollectionsOff = language.key( + localization.MessageDisplayOptionsSwitchButtonCollectionsOff + ); + const MessageDisplayOptionsSwitchButtonCollectionsOn = language.key( + localization.MessageDisplayOptionsSwitchButtonCollectionsOn + ); + const MessageDisplayOptionsSwitchButtonIsSingleItemOff = language.key( + localization.MessageDisplayOptionsSwitchButtonIsSingleItemOff + ); + const MessageDisplayOptionsSwitchButtonIsSingleItemOn = language.key( + localization.MessageDisplayOptionsSwitchButtonIsSingleItemOn + ); + const MessageDisplayOptionsSwitchButtonIsSocketOn = language.key( + localization.MessageDisplayOptionsSwitchButtonIsSocketOn + ); + const MessageDisplayOptionsSwitchButtonIsSocketOff = language.key( + localization.MessageDisplayOptionsSwitchButtonIsSocketOff + ); const history = useLocation(); @@ -98,9 +111,9 @@ const ModalDisplayOptions: React.FunctionComponent = isOn={!collections} data-test="toggle-collections" isEnabled={true} - leftLabel={MessageSwitchButtonCollectionsOn} + leftLabel={MessageDisplayOptionsSwitchButtonCollectionsOn} onToggle={() => toggleCollections()} - rightLabel={MessageSwitchButtonCollectionsOff} + rightLabel={MessageDisplayOptionsSwitchButtonCollectionsOff} />
@@ -108,8 +121,8 @@ const ModalDisplayOptions: React.FunctionComponent = data-test="toggle-slow-files" isOn={isAlwaysLoadImage} isEnabled={true} - leftLabel={MessageSwitchButtonIsSingleItemOn} - rightLabel={MessageSwitchButtonIsSingleItemOff} + leftLabel={MessageDisplayOptionsSwitchButtonIsSingleItemOn} + rightLabel={MessageDisplayOptionsSwitchButtonIsSingleItemOff} onToggle={() => toggleSlowFiles()} />
@@ -118,9 +131,9 @@ const ModalDisplayOptions: React.FunctionComponent = isOn={isUseSockets} data-test="toggle-sockets" isEnabled={true} - leftLabel={MessageSwitchButtonIsSocketOn} + leftLabel={MessageDisplayOptionsSwitchButtonIsSocketOn} onToggle={() => toggleSockets()} - rightLabel={MessageSwitchButtonIsSocketOff} + rightLabel={MessageDisplayOptionsSwitchButtonIsSocketOff} />
diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-download/modal-download.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-download/modal-download.tsx index 5f6e908b2a..f55fb74f2d 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-download/modal-download.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-download/modal-download.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import useFetch from "../../../hooks/use-fetch"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useInterval from "../../../hooks/use-interval"; +import localization from "../../../localization/localization.json"; import { ExportIntervalUpdate } from "../../../shared/export/export-interval-update"; import FetchPost from "../../../shared/fetch/fetch-post"; import { FileExtensions } from "../../../shared/file-extensions"; @@ -32,25 +33,13 @@ const ModalDownload: React.FunctionComponent = (props) => { // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageDownloadSelection = language.text("Download selectie", "Download selection"); - const MessageOriginalFile = language.text("Origineel bestand", "Original file"); - const MessageThumbnailFile = "Thumbnail"; - const MessageGenericExportFail = language.text( - "Er is iets misgegaan met exporteren", - "Something went wrong with exporting" - ); - const MessageExportReady = language.text( - "Het bestand {createZipKey} is klaar met exporteren.", - "The file {createZipKey} has finished exporting." - ); - const MessageDownloadAsZipArchive = language.text( - "Download als zip-archief", - "Download as a zip archive" - ); - const MessageOneMomentPlease = language.text( - "Een moment geduld alstublieft", - "One moment please" - ); + const MessageDownloadSelection = language.key(localization.MessageDownloadSelection); + const MessageOriginalFile = language.key(localization.MessageOriginalFile); + const MessageThumbnailFile = language.key(localization.MessageThumbnailFile); + const MessageGenericExportFail = language.key(localization.MessageGenericExportFail); + const MessageExportReady = language.key(localization.MessageExportReady); + const MessageDownloadAsZipArchive = language.key(localization.MessageDownloadAsZipArchive); + const MessageOneMomentPlease = language.key(localization.MessageOneMomentPlease); const [isProcessing, setIsProcessing] = React.useState(ProcessingState.default); const [createZipKey, setCreateZipKey] = React.useState(""); diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-edit-date-time/modal-edit-datetime.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-edit-date-time/modal-edit-datetime.tsx index 932b654468..6effd32c61 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-edit-date-time/modal-edit-datetime.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-edit-date-time/modal-edit-datetime.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; import { IFileIndexItem } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; import { isValidDate, leftPad, @@ -28,15 +29,12 @@ const ModalEditDatetime: React.FunctionComponent = (props) // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageModalDatetime = language.text("Datum en tijd bewerken", "Edit date and time"); - const MessageYear = language.text("Jaar", "Year"); - const MessageMonth = language.text("Maand", "Month"); - const MessageDate = language.text("Dag", "Day"); - const MessageTime = language.text("Tijd", "Time"); - const MessageErrorDatetime = language.text( - "De datum en tijd zijn incorrect ingegeven", - "The date and time were entered incorrectly" - ); + const MessageModalDatetime = language.key(localization.MessageModalDatetime); + const MessageYear = language.key(localization.MessageYear); + const MessageMonth = language.key(localization.MessageMonth); + const MessageDate = language.key(localization.MessageDate); + const MessageTime = language.key(localization.MessageTime); + const MessageErrorDatetime = language.key(localization.MessageErrorDatetime); const isFormEnabled = true; diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-force-delete/modal-force-delete.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-force-delete/modal-force-delete.tsx index a7d05f92ad..a26f03ff1e 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-force-delete/modal-force-delete.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-force-delete/modal-force-delete.tsx @@ -2,6 +2,7 @@ import { ArchiveAction } from "../../../contexts/archive-context"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useLocation from "../../../hooks/use-location/use-location"; import { IArchiveProps } from "../../../interfaces/IArchiveProps"; +import localization from "../../../localization/localization.json"; import FetchPost from "../../../shared/fetch/fetch-post"; import { Language } from "../../../shared/language"; import { ClearSearchCache } from "../../../shared/search/clear-search-cache"; @@ -32,12 +33,10 @@ const ModalForceDelete: React.FunctionComponent = ({ // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageDeleteIntroText = language.text( - "Weet je zeker dat je dit bestand wilt verwijderen van alle devices?", - "Are you sure you want to delete this file from all devices?" - ); - const MessageCancel = language.text("Annuleren", "Cancel"); - const MessageDeleteImmediately = language.text("Verwijder onmiddellijk", "Delete immediately"); + const MessageDeleteIntroText = language.key(localization.MessageDeleteIntroText); + const MessageCancel = language.key(localization.MessageCancel); + const MessageDeleteImmediately = language.key(localization.MessageDeleteImmediately); + const history = useLocation(); const undoSelection = () => new Select(select, setSelect, state, history).undoSelection(); diff --git a/starsky/starsky/clientapp/src/components/organisms/modal-publish/modal-publish.tsx b/starsky/starsky/clientapp/src/components/organisms/modal-publish/modal-publish.tsx index 8a754e8f9c..a5a248e5bf 100644 --- a/starsky/starsky/clientapp/src/components/organisms/modal-publish/modal-publish.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/modal-publish/modal-publish.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import useFetch from "../../../hooks/use-fetch"; import useGlobalSettings from "../../../hooks/use-global-settings"; import useInterval from "../../../hooks/use-interval"; +import localization from "../../../localization/localization.json"; import { ExportIntervalUpdate } from "../../../shared/export/export-interval-update"; import { ProcessingState } from "../../../shared/export/processing-state"; import FetchGet from "../../../shared/fetch/fetch-get"; @@ -27,35 +28,17 @@ const ModalPublish: React.FunctionComponent = (props) => { // content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessagePublishSelection = language.text("Publiceer selectie", "Publish selection"); - const MessageGenericExportFail = language.text( - "Er is iets misgegaan met exporteren", - "Something went wrong with exporting" - ); - const MessageRetryExportFail = language.text("Probeer het opnieuw", "Retry this"); - - const MessageExportReady = language.text( - "Het bestand {createZipKey} is klaar met exporteren.", - "The file {createZipKey} has finished exporting." - ); - const MessageDownloadAsZipArchive = language.text( - "Download als zip-archief", - "Download as a zip archive" - ); - const MessageOneMomentPlease = language.text( - "Een moment geduld alstublieft", - "One moment please" - ); - const MessageItemName = language.text("Waar gaat het item over?", "What is the item about?"); - const MessageItemNameInUse = language.text( - "Deze naam is al in gebruik, kies een andere naam", - "This name is already in use, please choose another name" - ); - const MessagePublishProfileName = language.text("Profiel instelling", "Profile setting"); - - const MessagePublishProfileNamesErrored = language.text( - "Profiel instelling: {publishProfileNames} bevat bestand locatie fouten", - "Profile setting: {publishProfileNames} contains filepath errors" + const MessagePublishSelection = language.key(localization.MessagePublishSelection); + const MessageGenericExportFail = language.key(localization.MessageGenericExportFail); + const MessageRetryExportFail = language.key(localization.MessageRetryExportFail); + const MessageExportReady = language.key(localization.MessageExportReady); + const MessageDownloadAsZipArchive = language.key(localization.MessageDownloadAsZipArchive); + const MessageOneMomentPlease = language.key(localization.MessageOneMomentPlease); + const MessageItemName = language.key(localization.MessageItemName); + const MessageItemNameInUse = language.key(localization.MessageItemNameInUse); + const MessagePublishProfileName = language.key(localization.MessagePublishProfileName); + const MessagePublishProfileNamesErrored = language.key( + localization.MessagePublishProfileNamesErrored ); const [isProcessing, setIsProcessing] = React.useState(ProcessingState.default); diff --git a/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.spec.tsx new file mode 100644 index 0000000000..b7daae8fa4 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.spec.tsx @@ -0,0 +1,470 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { ChangeEvent } from "react"; +import * as useFetch from "../../../hooks/use-fetch"; +import { IAppSettings } from "../../../interfaces/IAppSettings"; +import { RawJpegMode } from "../../../interfaces/ICollectionsOpenType"; +import { IConnectionDefault } from "../../../interfaces/IConnectionDefault"; +import { ImageFormat } from "../../../interfaces/IFileIndexItem"; +import * as FetchPost from "../../../shared/fetch/fetch-post"; +import { UrlQuery } from "../../../shared/url-query"; +import * as FormControl from "../../atoms/form-control/form-control"; +import * as SwitchButton from "../../atoms/switch-button/switch-button"; +import PreferencesAppSettingsDesktop, { + ToggleCollections, + UpdateDefaultEditorPhotos +} from "./preference-app-settings-desktop"; + +describe("PreferencesAppSettingsDesktop", () => { + it("should render correctly with provided props", () => { + const switchButtonSpy = jest.spyOn(SwitchButton, "default").mockImplementationOnce(() => { + return <>; + }); + + render(); + + expect(switchButtonSpy).toHaveBeenCalled(); + }); + + it("should render MessageSwitchButtonDesktopApplicationDescription when appSettings.useLocalDesktop is true", () => { + const mockGetIConnectionDefaultAppSettings = { + statusCode: 200, + data: { + useLocalDesktop: true, + defaultDesktopEditor: [] + } + } as IConnectionDefault; + + const mockGetIConnectionDefaultPermissions = { + statusCode: 200, + data: null + } as IConnectionDefault; + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions); + + const component = render(); + + expect( + screen.findByTestId("preference-app-settings-desktop-use-local-desktop-true") + ).toBeTruthy(); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useFetchSpy).toHaveBeenCalledTimes(2); + + component.unmount(); + }); + + it("should render MessageSwitchButtonDesktopApplicationDescription when appSettings.useLocalDesktop is false", () => { + const mockGetIConnectionDefaultAppSettings = { + statusCode: 200, + data: { + useLocalDesktop: false, + defaultDesktopEditor: [] + } + } as IConnectionDefault; + + const mockGetIConnectionDefaultPermissions = { + statusCode: 200, + data: null + } as IConnectionDefault; + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions); + + const component = render(); + + expect( + screen.findByTestId("preference-app-settings-desktop-use-local-desktop-false") + ).toBeTruthy(); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useFetchSpy).toHaveBeenCalledTimes(2); + + component.unmount(); + }); + + it("get application path from useFetch and display", () => { + const mockGetIConnectionDefaultAppSettings = { + statusCode: 200, + data: { + useLocalDesktop: true, + defaultDesktopEditor: [ + { + applicationPath: "/test", + imageFormats: [ImageFormat.tiff] + } + ] + } as unknown as IAppSettings + } as IConnectionDefault; + + const formControlSpy = jest.spyOn(FormControl, "default").mockImplementationOnce(() => { + return <>; + }); + + const mockGetIConnectionDefaultPermissions = { + statusCode: 200, + data: null + } as IConnectionDefault; + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions); + + const component = render(); + + expect(formControlSpy).toHaveBeenCalled(); + expect(formControlSpy).toHaveBeenCalledWith( + { + children: "/test", + contentEditable: undefined, + name: "tags", + onBlur: expect.anything(), + spellcheck: true + }, + {} + ); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useFetchSpy).toHaveBeenCalledTimes(2); + + component.unmount(); + }); + + it("give message when done with SwitchButton", async () => { + const mockGetIConnectionDefaultAppSettings = { + statusCode: 200, + data: { + useLocalDesktop: true, + defaultDesktopEditor: [] + } + } as IConnectionDefault; + + const mockGetIConnectionDefaultPermissions = { + statusCode: 200, + data: null + } as IConnectionDefault; + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions) + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions) + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions); + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + + const switchButtonSpy = jest.spyOn(SwitchButton, "default").mockImplementationOnce((props) => { + return ; + }); + + const component = render(); + + expect(screen.queryByTestId("preference-app-settings-desktop-warning-box")).toBeFalsy(); + + fireEvent.click(screen.getByTestId("switch-button-spy")); + + await waitFor(() => { + expect(spyFetchPost).toHaveBeenCalled(); + expect(spyFetchPost).toHaveBeenCalledTimes(1); + expect(spyFetchPost).toHaveBeenCalledWith( + new UrlQuery().UrlApiAppSettings(), + "desktopCollectionsOpen=2" + ); + + expect(screen.getByTestId("preference-app-settings-desktop-warning-box")).toBeTruthy(); + }); + + expect(switchButtonSpy).toHaveBeenCalled(); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useFetchSpy).toHaveBeenCalledTimes(6); + + component.unmount(); + }); + + it("give message when done with FormControl", async () => { + const mockGetIConnectionDefaultAppSettings = { + statusCode: 200, + data: { + useLocalDesktop: true, + defaultDesktopEditor: [] + } + } as IConnectionDefault; + + const mockGetIConnectionDefaultPermissions = { + statusCode: 200, + data: null + } as IConnectionDefault; + const useFetchSpy = jest + .spyOn(useFetch, "default") + .mockReset() + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions) + .mockImplementationOnce(() => mockGetIConnectionDefaultAppSettings) + .mockImplementationOnce(() => mockGetIConnectionDefaultPermissions); + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + + const switchButtonSpy = jest.spyOn(FormControl, "default").mockImplementationOnce((props) => { + return ( + + ); + }); + + const component = render(); + + expect(screen.queryByTestId("preference-app-settings-desktop-warning-box")).toBeFalsy(); + + fireEvent.click(screen.getByTestId("form-control-spy")); + + await waitFor(() => { + const query = + "DefaultDesktopEditor%5B0%5D.ImageFormats%5B0%5D=jpg&DefaultDesktopEditor%5B0%5D.ImageFormats%" + + "5B1%5D=png&DefaultDesktopEditor%5B0%5D.ImageFormats%5B2%5D=bmp&DefaultDesktopEditor%5B0%5D.ImageFormats%5B3%5D=tiff&" + + "DefaultDesktopEditor%5B0%5D.ApplicationPath=test"; + + expect(spyFetchPost).toHaveBeenCalledTimes(1); + expect(spyFetchPost).toHaveBeenCalledWith(new UrlQuery().UrlApiAppSettings(), query); + + expect(screen.getByTestId("preference-app-settings-desktop-warning-box")).toBeTruthy(); + }); + + expect(switchButtonSpy).toHaveBeenCalled(); + + expect(useFetchSpy).toHaveBeenCalled(); + expect(useFetchSpy).toHaveBeenCalledTimes(4); + + component.unmount(); + }); +}); + +describe("updateDefaultEditorPhotos", () => { + it("should call FetchPost with correct URL and bodyParams for updateDefaultEditorPhotos function", async () => { + const value = { + target: { innerText: "NewApplicationPath" } + } as unknown as ChangeEvent; + const defaultDesktopEditor = [ + { + applicationPath: "SampleApplicationPath", + imageFormats: [ImageFormat.jpg, ImageFormat.png] + } + ]; + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + await UpdateDefaultEditorPhotos(value, jest.fn(), "", "", defaultDesktopEditor); + + expect(spyFetchPost).toHaveBeenCalledWith( + expect.stringContaining(new UrlQuery().UrlApiAppSettings()), + expect.any(String) + ); + }); + + it("UpdateDefaultEditorPhotos call error if default defaultDesktopEditor is missing", async () => { + const value = { + target: { innerText: "NewApplicationPath" } + } as unknown as ChangeEvent; + const setIsMessage = jest.fn(); + await UpdateDefaultEditorPhotos(value, setIsMessage, "error_here", ""); + expect(setIsMessage).toHaveBeenCalled(); + expect(setIsMessage).toHaveBeenCalledWith("error_here"); + }); + + it("UpdateDefaultEditorPhotos call error if fetchPost is Error 500", async () => { + const value = { + target: { innerText: "NewApplicationPath" } + } as unknown as ChangeEvent; + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 500 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const setIsMessage = jest.fn(); + await UpdateDefaultEditorPhotos(value, setIsMessage, "error_here", "", []); + expect(setIsMessage).toHaveBeenCalled(); + expect(setIsMessage).toHaveBeenCalledWith("error_here"); + + expect(spyFetchPost).toHaveBeenCalled(); + }); + + it("Create new item in Array if emthy array", async () => { + const value = { + target: { innerText: "test" } + } as unknown as ChangeEvent; + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + + const setIsMessage = jest.fn(); + await UpdateDefaultEditorPhotos(value, setIsMessage, "error_here", "success", []); + expect(setIsMessage).toHaveBeenCalled(); + expect(setIsMessage).toHaveBeenCalledWith("success"); + + expect(spyFetchPost).toHaveBeenCalled(); + const query = + "DefaultDesktopEditor%5B0%5D.ImageFormats%5B0%5D=jpg&DefaultDesktopEditor%5B0%5D.ImageFormats%" + + "5B1%5D=png&DefaultDesktopEditor%5B0%5D.ImageFormats%5B2%5D=bmp&DefaultDesktopEditor%5B0%5D.ImageFormats%5B3%5D=tiff&" + + "DefaultDesktopEditor%5B0%5D.ApplicationPath=test"; + expect(spyFetchPost).toHaveBeenCalledWith(new UrlQuery().UrlApiAppSettings(), query); + }); + + it("Create new item in Array if emthy array", async () => { + const value = { + target: { innerText: "test" } + } as unknown as ChangeEvent; + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + + const setIsMessage = jest.fn(); + await UpdateDefaultEditorPhotos(value, setIsMessage, "error_here", "success", [ + { + applicationPath: "/exist_app", + imageFormats: [ImageFormat.gif] + } + ]); + expect(setIsMessage).toHaveBeenCalled(); + expect(setIsMessage).toHaveBeenCalledWith("success"); + + expect(spyFetchPost).toHaveBeenCalled(); + + const query2 = + "DefaultDesktopEditor%5B0%5D.ImageFormats%5B0%5D=jpg&DefaultDesktopEditor%5B0%5D.ImageFormats%5B1%5D=png&DefaultDesktopEditor" + + "%5B0%5D.ImageFormats%5B2%5D=bmp&DefaultDesktopEditor%5B0%5D.ImageFormats%5B3%5D=tiff&DefaultDesktopEditor%5B0%5D.ApplicationPath=%2Fexist_app&" + + "DefaultDesktopEditor%5B1%5D.ImageFormats%5B0%5D=jpg&DefaultDesktopEditor%5B1%5D.ImageFormats%5B1%5D=png&DefaultDesktopEditor%5B1%5D.ImageFormats%5B2%5D=bmp&" + + "DefaultDesktopEditor%5B1%5D.ImageFormats%5B3%5D=tiff&DefaultDesktopEditor%5B1%5D.ApplicationPath=test"; + + expect(spyFetchPost).toHaveBeenCalledWith(new UrlQuery().UrlApiAppSettings(), query2); + }); +}); + +describe("ToggleCollections", () => { + it("should call FetchPost with correct URL and bodyParams for toggleCollections function Jpeg", async () => { + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + const appSettings = { + desktopCollectionsOpen: RawJpegMode.Jpeg, + useLocalDesktop: true + } as unknown as IAppSettings; + + await ToggleCollections(true, jest.fn(), "", "", appSettings); + + expect(spyFetchPost).toHaveBeenCalledWith( + expect.stringContaining(new UrlQuery().UrlApiAppSettings()), + "desktopCollectionsOpen=2" + ); + }); + + it("should call FetchPost with correct URL and bodyParams for toggleCollections function Raw", async () => { + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 200 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockReset() + .mockImplementationOnce(() => mockIConnectionDefault); + const appSettings = { + desktopCollectionsOpen: RawJpegMode.Raw, + useLocalDesktop: true + } as unknown as IAppSettings; + + await ToggleCollections(false, jest.fn(), "", "", appSettings); + + expect(spyFetchPost).toHaveBeenCalledWith( + expect.stringContaining(new UrlQuery().UrlApiAppSettings()), + "desktopCollectionsOpen=1" + ); + }); + + it("ToggleCollections call error if fetchPost is Error 500", async () => { + const appSettings = { + desktopCollectionsOpen: RawJpegMode.Default, + useLocalDesktop: true + } as unknown as IAppSettings; + + const mockIConnectionDefault: Promise = Promise.resolve({ + data: null, + statusCode: 500 + }); + + const spyFetchPost = jest + .spyOn(FetchPost, "default") + .mockImplementationOnce(() => mockIConnectionDefault); + + const setIsMessage = jest.fn(); + await ToggleCollections(true, setIsMessage, "error_here", "", appSettings); + + expect(setIsMessage).toHaveBeenCalled(); + expect(setIsMessage).toHaveBeenCalledWith("error_here"); + + expect(spyFetchPost).toHaveBeenCalled(); + }); +}); diff --git a/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.tsx b/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.tsx new file mode 100644 index 0000000000..3cf849a4ec --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/preference-app-settings-desktop/preference-app-settings-desktop.tsx @@ -0,0 +1,174 @@ +import React, { ChangeEvent, useState } from "react"; +import useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IAppSettings } from "../../../interfaces/IAppSettings"; +import { IAppSettingsDefaultEditorApplication } from "../../../interfaces/IAppSettingsDefaultEditorApplication"; +import { RawJpegMode } from "../../../interfaces/ICollectionsOpenType"; +import { ImageFormat } from "../../../interfaces/IFileIndexItem"; +import localization from "../../../localization/localization.json"; +import FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { UrlQuery } from "../../../shared/url-query"; +import FormControl from "../../atoms/form-control/form-control"; +import SwitchButton from "../../atoms/switch-button/switch-button"; + +const defaultEditorApplication = { + imageFormats: [ImageFormat.jpg, ImageFormat.png, ImageFormat.bmp, ImageFormat.tiff] +} as IAppSettingsDefaultEditorApplication; + +export async function UpdateDefaultEditorPhotos( + event: ChangeEvent, + setIsMessage: React.Dispatch>, + MessageSwitchButtonDesktopCollectionsUpdateError: string, + MessageSwitchButtonDesktopCollectionsUpdateSuccess: string, + defaultDesktopEditor?: IAppSettingsDefaultEditorApplication[] +) { + if (!defaultDesktopEditor) { + setIsMessage(MessageSwitchButtonDesktopCollectionsUpdateError); + return; + } + const bodyParams = new URLSearchParams(); + + defaultEditorApplication.applicationPath = event.target.innerText; + const index = defaultDesktopEditor.findIndex( + (p) => p.imageFormats.includes(ImageFormat.jpg) || p.imageFormats.includes(ImageFormat.tiff) + ); + if (index === -1) { + defaultDesktopEditor.push(defaultEditorApplication); + } else { + defaultDesktopEditor[index] = defaultEditorApplication; + } + + defaultDesktopEditor.forEach((editorApp, index) => { + defaultEditorApplication.imageFormats.forEach((imageFormat, idx) => { + bodyParams.append( + `DefaultDesktopEditor[${index}].ImageFormats[${idx}]`, + imageFormat.toString() + ); + }); + bodyParams.append(`DefaultDesktopEditor[${index}].ApplicationPath`, editorApp.applicationPath); + }); + + const result = await FetchPost(new UrlQuery().UrlApiAppSettings(), bodyParams.toString()); + if (result.statusCode != 200) { + setIsMessage(MessageSwitchButtonDesktopCollectionsUpdateError); + return; + } + setIsMessage(MessageSwitchButtonDesktopCollectionsUpdateSuccess); +} + +export async function ToggleCollections( + value: boolean, + setIsMessage: React.Dispatch>, + MessageSwitchButtonDesktopCollectionsUpdateError: string, + MessageSwitchButtonDesktopCollectionsUpdateSuccess: string, + appSettings: IAppSettings | null +) { + const desktopCollectionsOpen = value ? RawJpegMode.Raw : RawJpegMode.Jpeg; + + const bodyParams = new URLSearchParams(); + bodyParams.set("desktopCollectionsOpen", desktopCollectionsOpen.toString()); + + const result = await FetchPost(new UrlQuery().UrlApiAppSettings(), bodyParams.toString()); + if (result.statusCode != 200 || !appSettings) { + setIsMessage(MessageSwitchButtonDesktopCollectionsUpdateError); + return; + } + // to avoid re-render issues to display message + appSettings.desktopCollectionsOpen = desktopCollectionsOpen; + setIsMessage(MessageSwitchButtonDesktopCollectionsUpdateSuccess); +} + +const PreferencesAppSettingsDesktop: React.FunctionComponent = () => { + // Get AppSettings from backend + const appSettings = useFetch(new UrlQuery().UrlApiAppSettings(), "get") + ?.data as IAppSettings | null; + // roles + const permissionsData = useFetch(new UrlQuery().UrlAccountPermissions(), "get"); + + const isAppSettingsWrite = permissionsData?.data?.includes( + new UrlQuery().KeyAccountPermissionAppSettingsWrite() + ); + + const settings = useGlobalSettings(); + + const imageDefaultEditor = appSettings?.defaultDesktopEditor.find( + (p) => p.imageFormats.includes(ImageFormat.jpg) || p.imageFormats.includes(ImageFormat.tiff) + ); + + const language = new Language(settings.language); + const MessageSwitchButtonDesktopApplication = language.key( + localization.MessageSwitchButtonDesktopApplication + ); + const MessageSwitchButtonDesktopApplicationDescription = language.key( + localization.MessageSwitchButtonDesktopApplicationDescription + ); + + // for showing a notification + const [isMessage, setIsMessage] = useState(""); + + return ( + <> +
{MessageSwitchButtonDesktopApplication}
+
+
+ {MessageSwitchButtonDesktopApplicationDescription} +
+ + {isMessage !== "" ? ( +
+ {isMessage} +
+ ) : null} + + ToggleCollections( + value, + setIsMessage, + language.key(localization.MessageSwitchButtonDesktopCollectionsRawJpegUpdateError), + language.key(localization.MessageSwitchButtonDesktopCollectionsRawJpegUpdateSuccess), + appSettings + ) + } + rightLabel={language.key(localization.MessageSwitchButtonDesktopCollectionsRawOn)} + /> +
+
+

{language.key(localization.MessageAppSettingDefaultEditorPhotos)}

+

{language.key(localization.MessageAppSettingDefaultEditorPhotosDescription)}

+ + UpdateDefaultEditorPhotos( + value, + setIsMessage, + language.key(localization.MessageSwitchButtonDesktopCollectionsUpdateError), + language.key(localization.MessageSwitchButtonDesktopCollectionsUpdateSuccess), + appSettings?.defaultDesktopEditor + ) + } + name="tags" + contentEditable={appSettings?.useLocalDesktop === true && isAppSettingsWrite} + > + {imageDefaultEditor?.applicationPath} + +
+ + ); +}; + +export default PreferencesAppSettingsDesktop; diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.spec.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.spec.tsx similarity index 58% rename from starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.spec.tsx rename to starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.spec.tsx index 3ebb72c1da..616345d77f 100644 --- a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.spec.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.spec.tsx @@ -1,62 +1,18 @@ -import { act, createEvent, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { act, createEvent, fireEvent, render, screen } from "@testing-library/react"; import * as useFetch from "../../../hooks/use-fetch"; import { IConnectionDefault, newIConnectionDefault } from "../../../interfaces/IConnectionDefault"; import * as FetchPost from "../../../shared/fetch/fetch-post"; import { UrlQuery } from "../../../shared/url-query"; -import PreferencesAppSettings, { ChangeSetting } from "./preferences-app-settings"; +import PreferencesAppSettingsStorageFolder, { + ChangeSetting +} from "./preferences-app-settings-storage-folder"; describe("PreferencesAppSettings", () => { it("renders", () => { - render(); + render(); }); describe("context", () => { - it("disabled by default", () => { - // usage ==> import * as useFetch from '../../../hooks/use-fetch'; - jest - .spyOn(useFetch, "default") - .mockImplementationOnce(() => newIConnectionDefault()) - .mockImplementationOnce(() => newIConnectionDefault()); - - const component = render(); - - const switchButtons = screen.queryAllByTestId("switch-button-right"); - - const verbose = switchButtons.find( - (p) => p.getAttribute("name") === "verbose" - ) as HTMLInputElement; - - expect(verbose.disabled).toBeTruthy(); - - component.unmount(); - }); - - it("not disabled when admin", () => { - const connectionDefault = { - statusCode: 200, - data: ["AppSettingsWrite"] - } as IConnectionDefault; - // usage ==> import * as useFetch from '../../../hooks/use-fetch'; - jest - .spyOn(useFetch, "default") - .mockImplementationOnce(() => connectionDefault) - .mockImplementationOnce(() => connectionDefault) - .mockImplementationOnce(() => connectionDefault) - .mockImplementationOnce(() => connectionDefault); - - const component = render(); - - const switchButtons = screen.queryAllByTestId("switch-button-right"); - - const verbose = switchButtons.find( - (p) => p.getAttribute("name") === "verbose" - ) as HTMLInputElement; - - expect(verbose.disabled).toBeFalsy(); - - component.unmount(); - }); - it("filled right data", () => { const permissions = { statusCode: 200, @@ -78,7 +34,7 @@ describe("PreferencesAppSettings", () => { .mockImplementationOnce(() => permissions) .mockImplementationOnce(() => appSettings); - const component = render(); + const component = render(); const formControls = screen.queryAllByTestId("form-control"); @@ -124,7 +80,7 @@ describe("PreferencesAppSettings", () => { .spyOn(FetchPost, "default") .mockImplementationOnce(() => mockIConnectionDefault); - const component = render(); + const component = render(); const formControls = screen .queryAllByTestId("form-control") @@ -181,7 +137,7 @@ describe("PreferencesAppSettings", () => { .spyOn(FetchPost, "default") .mockImplementationOnce(() => mockIConnectionDefault); - const component = render(); + const component = render(); const formControls = screen .queryAllByTestId("form-control") @@ -210,76 +166,9 @@ describe("PreferencesAppSettings", () => { component.unmount(); }); }); - - it("toggle verbose", async () => { - const permissions = { - statusCode: 200, - data: ["AppSettingsWrite"] - } as IConnectionDefault; - const appSettings = { - statusCode: 200, - data: { - verbose: true, - storageFolder: "test" - } - } as IConnectionDefault; - - // usage ==> import * as useFetch from '../../../hooks/use-fetch'; - jest - .spyOn(useFetch, "default") - .mockReset() - .mockImplementationOnce(() => permissions) - .mockImplementationOnce(() => appSettings) - .mockImplementationOnce(() => permissions) - .mockImplementationOnce(() => appSettings) - .mockImplementationOnce(() => permissions) - .mockImplementationOnce(() => appSettings) - .mockImplementationOnce(() => permissions) - .mockImplementationOnce(() => appSettings) - .mockImplementationOnce(() => permissions) - .mockImplementationOnce(() => appSettings); - - const component = render(); - - const fetchPostSpy = jest - .spyOn(FetchPost, "default") - .mockReset() - .mockImplementationOnce(() => { - return Promise.resolve({ - statusCode: 400, - data: null - }); - }); - - const switchButtons = screen.queryAllByTestId("switch-button-right"); - - const verbose = switchButtons.find( - (p) => p.getAttribute("name") === "verbose" - ) as HTMLElement; - - verbose.click(); - - await waitFor(() => expect(fetchPostSpy).toHaveBeenCalled()); - - component.unmount(); - }); }); describe("ChangeSetting", () => { - it("should set value with empty string as name when name is not provided", async () => { - const value = "test value"; - const fetchPostSpy = jest.spyOn(FetchPost, "default").mockImplementationOnce(() => { - return Promise.resolve({ - statusCode: 200, - data: null - }); - }); - - const statusCode = await ChangeSetting(value); - expect(statusCode).toBe(200); - expect(fetchPostSpy).toHaveBeenCalled(); - }); - it("should set value with provided name when name is provided", async () => { const value = "test value"; const name = "test name"; diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.stories.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.stories.tsx new file mode 100644 index 0000000000..aa67399455 --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.stories.tsx @@ -0,0 +1,11 @@ +import PreferencesAppSettingsStorageFolder from "./preferences-app-settings-storage-folder"; + +export default { + title: "components/organisms/preferences-app-settings-storage-folder" +}; + +export const Default = () => { + return ; +}; + +Default.storyName = "default"; diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.tsx new file mode 100644 index 0000000000..6ea84e279e --- /dev/null +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings-storage-folder/preferences-app-settings-storage-folder.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from "react"; +import useFetch from "../../../hooks/use-fetch"; +import useGlobalSettings from "../../../hooks/use-global-settings"; +import { IAppSettings } from "../../../interfaces/IAppSettings"; +import localization from "../../../localization/localization.json"; +import FetchPost from "../../../shared/fetch/fetch-post"; +import { Language } from "../../../shared/language"; +import { UrlQuery } from "../../../shared/url-query"; +import FormControl from "../../atoms/form-control/form-control"; + +/** + * Update Change Settings + * @param value - content + * @param name - key name + * @returns void + */ +export async function ChangeSetting(value: string, name?: string): Promise { + const bodyParams = new URLSearchParams(); + bodyParams.set(name ?? "", value); + const result = await FetchPost(new UrlQuery().UrlApiAppSettings(), bodyParams.toString()); + return result?.statusCode; +} + +const PreferencesAppSettingsStorageFolder: React.FunctionComponent = () => { + const settings = useGlobalSettings(); + const language = new Language(settings.language); + const MessageAppSettingsEntireAppScope = language.key( + localization.MessageAppSettingsEntireAppScope + ); + const MessageChangeNeedReSync = language.key(localization.MessageChangeNeedReSync); + const MessageAppSettingsStorageFolder = language.key( + localization.MessageAppSettingsStorageFolder + ); + const MessageAppSettingStorageFolderSaveFail = language.key( + localization.MessageAppSettingStorageFolderSaveFail + ); + const MessageAppSettingStorageFolderEnvUsedFail = language.key( + localization.MessageAppSettingStorageFolderEnvUsedFail + ); + + const permissionsData = useFetch(new UrlQuery().UrlAccountPermissions(), "get"); + + const [isEnabled, setIsEnabled] = useState(false); + + useEffect(() => { + function permissions(): boolean { + if (!permissionsData?.data?.includes || permissionsData?.statusCode !== 200) { + return false; + } + // AppSettingsWrite + return permissionsData.data.includes(new UrlQuery().KeyAccountPermissionAppSettingsWrite()); + } + + setIsEnabled(permissions()); + }, [permissionsData]); + + const [storageFolderNotFound, setStorageFolderNotFound] = useState(false); + + const appSettings = useFetch(new UrlQuery().UrlApiAppSettings(), "get") + ?.data as IAppSettings | null; + + const [storageFolder, setStorageFolder] = useState(appSettings?.storageFolder); + + useEffect(() => { + setStorageFolder(appSettings?.storageFolder); + }, [appSettings]); + + return ( + <> +
+ {MessageAppSettingsEntireAppScope} +
+

{MessageAppSettingsStorageFolder}

+ { + const resultStatusCode = await ChangeSetting(e.target.innerText, "storageFolder"); + setStorageFolder(e.target.innerText); + setStorageFolderNotFound(resultStatusCode === 404); + }} + contentEditable={isEnabled && appSettings?.storageFolderAllowEdit === true} + > + {storageFolder} + + + {storageFolderNotFound ? ( +
+ {MessageAppSettingStorageFolderSaveFail} +
+ ) : null} + + {storageFolder !== appSettings?.storageFolder && !storageFolderNotFound ? ( +
+ {MessageChangeNeedReSync} +
+ ) : null} + + {appSettings?.storageFolderAllowEdit !== true ? ( +
+ {MessageAppSettingStorageFolderEnvUsedFail} +
+ ) : null} + + ); +}; + +export default PreferencesAppSettingsStorageFolder; diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.tsx index 6e2757021e..428a5b2c4f 100644 --- a/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-app-settings/preferences-app-settings.tsx @@ -1,111 +1,14 @@ -import React, { useEffect, useState } from "react"; -import useFetch from "../../../hooks/use-fetch"; -import useGlobalSettings from "../../../hooks/use-global-settings"; -import { IAppSettings } from "../../../interfaces/IAppSettings"; -import FetchPost from "../../../shared/fetch/fetch-post"; -import { Language } from "../../../shared/language"; -import { UrlQuery } from "../../../shared/url-query"; -import FormControl from "../../atoms/form-control/form-control"; -import SwitchButton from "../../atoms/switch-button/switch-button"; - -export async function ChangeSetting(value: string, name?: string): Promise { - const bodyParams = new URLSearchParams(); - bodyParams.set(name ?? "", value); - const result = await FetchPost(new UrlQuery().UrlApiAppSettings(), bodyParams.toString()); - return result?.statusCode; -} - -const PreferencesAppSettings: React.FunctionComponent = () => { - const settings = useGlobalSettings(); - const language = new Language(settings.language); - const MessageAppSettingsEntireAppScope = language.text( - "De AppSettings mogen alleen worden aangepast door Administrators. " + - "Deze instellingen worden toegepast voor de gehele applicatie ", - "The AppSettings may only be modified by Administrators. These settings are applied for the entire application" - ); - const MessageChangeNeedReSync = language.text( - "Je hebt deze instelling veranderd, nu dien je een volledige sync uit te voeren. Ga naar de hoofdmap, het meer-menu en klik op handmatig synchroniseren.", - "You have changed this setting, now you need to perform a full sync. Go to the root folder, the more menu and click on manual sync." - ); - - const permissionsData = useFetch(new UrlQuery().UrlAccountPermissions(), "get"); - - const [isEnabled, setIsEnabled] = useState(false); - - useEffect(() => { - function permissions(): boolean { - if (!permissionsData?.data?.includes || permissionsData?.statusCode !== 200) { - return false; - } - return permissionsData.data.includes("AppSettingsWrite"); - } - - setIsEnabled(permissions()); - }, [permissionsData]); - - const [storageFolderNotFound, setStorageFolderNotFound] = useState(false); - - const appSettings = useFetch(new UrlQuery().UrlApiAppSettings(), "get") - ?.data as IAppSettings | null; - - const [isVerbose, setIsVerbose] = useState(appSettings?.verbose); - const [storageFolder, setStorageFolder] = useState(appSettings?.storageFolder); - - useEffect(() => { - setIsVerbose(appSettings?.verbose); - setStorageFolder(appSettings?.storageFolder); - }, [appSettings]); +import React from "react"; +import PreferencesAppSettingsDesktop from "../preference-app-settings-desktop/preference-app-settings-desktop"; +import PreferencesAppSettingsStorageFolder from "../preferences-app-settings-storage-folder/preferences-app-settings-storage-folder"; +const PreferencesAppSettings: React.FunctionComponent = () => { return (
AppSettings
-
{MessageAppSettingsEntireAppScope}
- -

Verbose logging

- - { - ChangeSetting((!toggle).toString(), name); - setIsVerbose(!toggle); - }} - leftLabel={"on"} - name="verbose" - rightLabel={"off"} - /> - -

Storage Folder

- { - const resultStatusCode = await ChangeSetting(e.target.innerText, "storageFolder"); - setStorageFolder(e.target.innerText); - setStorageFolderNotFound(resultStatusCode === 404); - }} - contentEditable={isEnabled && appSettings?.storageFolderAllowEdit === true} - > - {storageFolder} - - - {storageFolderNotFound ? ( -
- Directory not found so not saved -
- ) : null} - - {storageFolder !== appSettings?.storageFolder && !storageFolderNotFound ? ( -
- {MessageChangeNeedReSync} -
- ) : null} - - {appSettings?.storageFolderAllowEdit !== true ? ( -
- You should update the Environment variable app__storageFolder -
- ) : null} + +
); diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-password/preferences-password.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-password/preferences-password.tsx index 877bc327d6..7f913cff4c 100644 --- a/starsky/starsky/clientapp/src/components/organisms/preferences-password/preferences-password.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-password/preferences-password.tsx @@ -1,44 +1,23 @@ import { useState } from "react"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import FetchPost from "../../../shared/fetch/fetch-post"; import { Language } from "../../../shared/language"; import { UrlQuery } from "../../../shared/url-query"; import ButtonStyled from "../../atoms/button-styled/button-styled"; -const PreferencesPassword: React.FunctionComponent = () => { +const PreferencesPassword: React.FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageChangePassword = language.text("Verander je wachtwoord", "Change your password"); - - const MessageExamplePassword = language.text("superveilig", "supersafe"); - const MessageCurrentPassword = language.text( - "Geef je huidige wachtwoord op", - "Enter your current password" - ); - const MessageChangedPassword = language.text( - "Geef je nieuwe wachtwoord op", - "Enter your new password" - ); - const MessageChangedConfirmPassword = language.text( - "Herhaal je nieuwe wachtwoord", - "And your new password again" - ); - const MessageNoPassword = language.text( - "Voer het huidige en nieuwe wachtwoord in", - "Enter the current and new password" - ); - const MessagePasswordChanged = language.text( - "Je wachtwoord is succesvol veranderd", - "Your password has been successfully changed" - ); - const MessagePasswordNoMatch = language.text( - "De wachtwoorden komen niet overeen", - "The passwords do not match" - ); - const MessagePasswordModalError = language.text( - "Het nieuwe wachtwoord voldoet niet aan de criteria", - "The new password does not meet the criteria" - ); + const MessageChangePassword = language.key(localization.MessageChangedPassword); + const MessageExamplePassword = language.key(localization.MessageExamplePassword); + const MessageCurrentPassword = language.key(localization.MessageCurrentPassword); + const MessageChangedPassword = language.key(localization.MessageChangedPassword); + const MessageChangedConfirmPassword = language.key(localization.MessageChangedConfirmPassword); + const MessageNoPassword = language.key(localization.MessageNoPassword); + const MessagePasswordChanged = language.key(localization.MessagePasswordChanged); + const MessagePasswordNoMatch = language.key(localization.MessagePasswordNoMatch); + const MessagePasswordModalError = language.key(localization.MessagePasswordModalError); const [loading, setLoading] = useState(false); diff --git a/starsky/starsky/clientapp/src/components/organisms/preferences-username/preferences-username.tsx b/starsky/starsky/clientapp/src/components/organisms/preferences-username/preferences-username.tsx index f9ce1ad529..e789e3d445 100644 --- a/starsky/starsky/clientapp/src/components/organisms/preferences-username/preferences-username.tsx +++ b/starsky/starsky/clientapp/src/components/organisms/preferences-username/preferences-username.tsx @@ -1,14 +1,16 @@ import React, { useEffect } from "react"; import useFetch from "../../../hooks/use-fetch"; import useGlobalSettings from "../../../hooks/use-global-settings"; +import localization from "../../../localization/localization.json"; import { Language } from "../../../shared/language"; import { UrlQuery } from "../../../shared/url-query"; -const PreferencesUsername: React.FunctionComponent = () => { +const PreferencesUsername: React.FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageUnknownUsername = language.text("Onbekende gebruikersnaam", "Unknown username"); - const MessageUsername = language.text("Gebruikersnaam", "Username"); + const MessageUnknownUsername = language.key(localization.MessageUnknownUsername); + const MessageUsername = language.key(localization.MessageUsername); + const MessageRole = language.key(localization.MessageRole); const accountStatus = useFetch(new UrlQuery().UrlAccountStatus(), "get"); const [userName, setUserName] = React.useState(MessageUnknownUsername); @@ -32,6 +34,8 @@ const PreferencesUsername: React.FunctionComponent = () => { > {userName}
+
{MessageRole}
+
{accountStatus?.data?.roleCode}
); }; diff --git a/starsky/starsky/clientapp/src/containers/account-register.tsx b/starsky/starsky/clientapp/src/containers/account-register.tsx index 9a6062145a..a63462235b 100644 --- a/starsky/starsky/clientapp/src/containers/account-register.tsx +++ b/starsky/starsky/clientapp/src/containers/account-register.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from "react"; import ButtonStyled from "../components/atoms/button-styled/button-styled"; import useGlobalSettings from "../hooks/use-global-settings"; import useLocation from "../hooks/use-location/use-location"; +import localization from "../localization/localization.json"; import { DocumentTitle } from "../shared/document-title"; import FetchGet from "../shared/fetch/fetch-get"; import FetchPost from "../shared/fetch/fetch-post"; @@ -14,54 +15,21 @@ const AccountRegister: FunctionComponent = () => { const language = new Language(settings.language); const MessageApplicationName = "Starsky"; - const MessageCreateNewAccount = language.text("Maak nieuw account", "Create new account"); - const MessageUsername = language.text("E-mailadres", "E-mail address"); - const MessageExamplePassword = language.text("superveilig", "supersafe"); - const MessageExampleUsername = "dont@mail.me"; - const MessagePassword = language.text("Geef je wachtwoord op", "Enter your password"); - const MessageConfirmPassword = language.text( - "Vul je wachtwoord nog een keer in", - "Enter your password again" - ); - const MessageNoUsernamePassword = language.text( - "Voer een emailadres en een wachtwoord in", - "Enter an email address and password" - ); - const MessageWrongFormatEmailAddress = language.text( - "Controleer je email adres", - "Check your email address" - ); - const MessagePasswordToShort = language.text( - "Gebruik minimaal 8 tekens voor je wachtwoord", - "Use at least 8 characters for your password" - ); - const MessagePasswordNoMatch = language.text( - "Deze wachtwoorden komen niet overeen. Probeer het opnieuw", - "These passwords do not match. Please try again" - ); - const MessageConnection = language.text( - "Er is geen verbinding mogelijk, probeer het later opnieuw", - "No connection is possible, please try again later" - ); - const MessageRejectedBadRequest = language.text( - "Dit verzoek is afgewezen aangezien er niet voldaan is aan de beveiligingseisen (Error 400)", - "This request was rejected because the security requirements were not met (Error 400)" - ); - const MessageRegistrationTurnedOff = language.text( - "Registratie is uitgezet", - "Registration is turned off" - ); - const MessageSignInInstead = language.text("In plaats daarvan inloggen", "Sign in instead"); - - const MessageLegalCreateAccountHtml = language.text( - `Door het creëren van een account gaat u akkoord met de - Algemene Voorwaarden van Starsky. Raadpleeg en bekijk hier onze - Privacykennisgeving en onze - Cookieverklaring.`, - `By creating an account you agree to Starsky's Conditions of Use. - Please see our Privacy Notice and our - Cookies Notice ` - ); + const MessageCreateNewAccount = language.key(localization.MessageCreateNewAccount); + const MessageUsername = language.key(localization.MessageUsername); + const MessageExamplePassword = language.key(localization.MessageExamplePassword); + const MessageExampleUsername = language.key(localization.MessageExampleUsername); + const MessagePassword = language.key(localization.MessagePassword); + const MessageConfirmPassword = language.key(localization.MessageConfirmPassword); + const MessageNoUsernamePassword = language.key(localization.MessageNoUsernamePassword); + const MessageWrongFormatEmailAddress = language.key(localization.MessageWrongFormatEmailAddress); + const MessagePasswordToShort = language.key(localization.MessagePasswordToShort); + const MessagePasswordNoMatch = language.key(localization.MessagePasswordNoMatch); + const MessageConnection = language.key(localization.MessageConnection); + const MessageRejectedBadRequest = language.key(localization.MessageRejectedBadRequest); + const MessageRegistrationTurnedOff = language.key(localization.MessageRegistrationTurnedOff); + const MessageSignInInstead = language.key(localization.MessageSignInInstead); + const MessageLegalCreateAccountHtml = language.key(localization.MessageLegalCreateAccountHtml); const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/containers/archive.spec.tsx b/starsky/starsky/clientapp/src/containers/archive/archive.spec.tsx similarity index 94% rename from starsky/starsky/clientapp/src/containers/archive.spec.tsx rename to starsky/starsky/clientapp/src/containers/archive/archive.spec.tsx index 018f259017..9b9925dd3e 100644 --- a/starsky/starsky/clientapp/src/containers/archive.spec.tsx +++ b/starsky/starsky/clientapp/src/containers/archive/archive.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from "@testing-library/react"; -import { newIArchive } from "../interfaces/IArchive"; +import { newIArchive } from "../../interfaces/IArchive"; import Archive from "./archive"; describe("Archive", () => { diff --git a/starsky/starsky/clientapp/src/containers/archive/archive.stories.tsx b/starsky/starsky/clientapp/src/containers/archive/archive.stories.tsx new file mode 100644 index 0000000000..550628bdee --- /dev/null +++ b/starsky/starsky/clientapp/src/containers/archive/archive.stories.tsx @@ -0,0 +1,28 @@ +import { MemoryRouter } from "react-router-dom"; +import { IArchiveProps } from "../../interfaces/IArchiveProps"; +import { Router } from "../../router-app/router-app"; +import Archive from "./archive"; + +export default { + title: "containers/archive" +}; + +export const Default = () => { + const archive = { + colorClassUsage: [1], + colorClassActiveList: [1], + fileIndexItems: [{ fileName: "test", filePath: "/test.jpg", colorClass: 1 }] + } as IArchiveProps; + + Router.navigate("?details=true&modal=false"); + + return ( + + + + ); +}; + +Default.story = { + name: "default" +}; diff --git a/starsky/starsky/clientapp/src/containers/archive.tsx b/starsky/starsky/clientapp/src/containers/archive/archive.tsx similarity index 65% rename from starsky/starsky/clientapp/src/containers/archive.tsx rename to starsky/starsky/clientapp/src/containers/archive/archive.tsx index 98e1bb6ad9..8f742b9038 100644 --- a/starsky/starsky/clientapp/src/containers/archive.tsx +++ b/starsky/starsky/clientapp/src/containers/archive/archive.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from "react"; -import ArchivePagination from "../components/molecules/archive-pagination/archive-pagination"; -import Breadcrumb from "../components/molecules/breadcrumbs/breadcrumbs"; -import ColorClassFilter from "../components/molecules/color-class-filter/color-class-filter"; -import ItemListView from "../components/molecules/item-list-view/item-list-view"; -import ArchiveSidebar from "../components/organisms/archive-sidebar/archive-sidebar"; -import MenuArchive from "../components/organisms/menu-archive/menu-archive"; -import useLocation from "../hooks/use-location/use-location"; -import { IArchiveProps } from "../interfaces/IArchiveProps"; -import { URLPath } from "../shared/url-path"; +import ArchivePagination from "../../components/molecules/archive-pagination/archive-pagination"; +import Breadcrumb from "../../components/molecules/breadcrumbs/breadcrumbs"; +import ColorClassFilter from "../../components/molecules/color-class-filter/color-class-filter"; +import ItemListView from "../../components/molecules/item-list-view/item-list-view"; +import ArchiveSidebar from "../../components/organisms/archive-sidebar/archive-sidebar"; +import MenuArchive from "../../components/organisms/menu-archive/menu-archive"; +import useLocation from "../../hooks/use-location/use-location"; +import { IArchiveProps } from "../../interfaces/IArchiveProps"; +import { URLPath } from "../../shared/url-path"; function Archive(archive: Readonly) { const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/containers/login.tsx b/starsky/starsky/clientapp/src/containers/login.tsx index 2c55d7775e..7c9e517887 100755 --- a/starsky/starsky/clientapp/src/containers/login.tsx +++ b/starsky/starsky/clientapp/src/containers/login.tsx @@ -4,6 +4,7 @@ import Preloader from "../components/atoms/preloader/preloader"; import useFetch from "../hooks/use-fetch"; import useGlobalSettings from "../hooks/use-global-settings"; import useLocation from "../hooks/use-location/use-location"; +import localization from "../localization/localization.json"; import { BrowserDetect } from "../shared/browser-detect"; import { DocumentTitle } from "../shared/document-title"; import FetchPost from "../shared/fetch/fetch-post"; @@ -30,40 +31,21 @@ export const Login: React.FC = () => { const language = new Language(settings.language); const MessageApplicationName = "Starsky"; - const MessageWrongUsernamePassword = language.text( - "Je gebruikersnaam of wachtwoord is niet juist. Probeer het opnieuw", - "Your username or password is incorrect. Try again" - ); - const MessageLockedOut = language.text( - "Je hebt te vaak geprobeerd in te loggen, probeer het over een uur nog een keer", - "You've tried to login too many times, please try again in an hour " - ); - - const MessageNoUsernamePassword = language.text( - "Voer een emailadres en een wachtwoord in", - "Enter an email address and password" - ); - const MessageWrongFormatEmailAddress = language.text( - "Controleer je email adres", - "Check your email address" - ); - const MessageUsername = language.text("E-mailadres", "E-mail address"); - const MessageConnection = language.text( - "Er is geen verbinding mogelijk, probeer het later opnieuw", - "No connection is possible, please try again later" - ); - const MessageDatabaseConnection = language.text( - "Er zijn problemen met de verbinding met de database. Controleer en pas de appsettings aan", - "There are database connection issues. Check and edit the appsettings" - ); - const LogoutWarning = language.text("Wil je uitloggen?", "Do you want to log out?"); - const MessageStayLoggedIn = language.text("Blijf ingelogd", "Stay logged in"); - const MessagePassword = language.text("Geef je wachtwoord op", "Enter your password"); - const MessageExamplePassword = language.text("superveilig", "supersafe"); - const MessageExampleUsername = "dont@mail.me"; - const MessageLogin = language.text("Inloggen", "Login"); - const MessageLogout = language.text("Uitloggen", "Logout"); - const MessageCreateAccount = language.text("Account maken", "Create account"); + const MessageWrongUsernamePassword = language.key(localization.MessageWrongUsernamePassword); + const MessageLockedOut = language.key(localization.MessageLockedOut); + const MessageNoUsernamePassword = language.key(localization.MessageNoUsernamePassword); + const MessageWrongFormatEmailAddress = language.key(localization.MessageWrongFormatEmailAddress); + const MessageUsername = language.key(localization.MessageUsername); + const MessageConnection = language.key(localization.MessageConnection); + const MessageDatabaseConnection = language.key(localization.MessageDatabaseConnection); + const LogoutWarning = language.key(localization.LogoutWarning); + const MessageStayLoggedIn = language.key(localization.MessageStayLoggedIn); + const MessagePassword = language.key(localization.MessagePassword); + const MessageExamplePassword = language.key(localization.MessageExamplePassword); + const MessageExampleUsername = language.key(localization.MessageExampleUsername); + const MessageLogin = language.key(localization.MessageLogin); + const MessageLogout = language.key(localization.MessageLogout); + const MessageCreateAccount = language.key(localization.MessageCreateAccount); // We don't want to login twice const [isLogin, setIsLogin] = React.useState(true); diff --git a/starsky/starsky/clientapp/src/containers/media-content.tsx b/starsky/starsky/clientapp/src/containers/media-content.tsx index 65d38b993f..6103fe9f3b 100644 --- a/starsky/starsky/clientapp/src/containers/media-content.tsx +++ b/starsky/starsky/clientapp/src/containers/media-content.tsx @@ -10,6 +10,7 @@ import useGlobalSettings from "../hooks/use-global-settings"; import useLocation from "../hooks/use-location/use-location"; import { IArchive } from "../interfaces/IArchive"; import { IDetailView, PageType } from "../interfaces/IDetailView"; +import localization from "../localization/localization.json"; import { NotFoundPage } from "../pages/not-found-page"; import { Language } from "../shared/language"; import { Login } from "./login"; @@ -27,10 +28,8 @@ const MediaContent: React.FC = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageConnectionRealtimeError = language.text( - "De verbinding is niet helemaal oké. We proberen het te herstellen", - "The connection is not quite right. We are trying to fix it" - ); + const MessageConnectionRealtimeError = language.key(localization.MessageConnectionRealtimeError); + const MessageApplicationFailed = language.key(localization.MessageApplicationFailed); console.log(`-----------------MediaContent ${pageType} (rendered again)-------------------`); @@ -38,7 +37,7 @@ const MediaContent: React.FC = () => { return ( <>
- The application has failed. Please reload it to try it again + {MessageApplicationFailed} ); } diff --git a/starsky/starsky/clientapp/src/containers/preferences/preferences.tsx b/starsky/starsky/clientapp/src/containers/preferences/preferences.tsx index f7b0dde01e..b5f9a2610a 100644 --- a/starsky/starsky/clientapp/src/containers/preferences/preferences.tsx +++ b/starsky/starsky/clientapp/src/containers/preferences/preferences.tsx @@ -3,12 +3,13 @@ import PreferencesAppSettings from "../../components/organisms/preferences-app-s import PreferencesPassword from "../../components/organisms/preferences-password/preferences-password"; import PreferencesUsername from "../../components/organisms/preferences-username/preferences-username"; import useGlobalSettings from "../../hooks/use-global-settings"; +import localization from "../../localization/localization.json"; import { Language } from "../../shared/language"; export const Preferences: React.FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessagePreferences = language.text("Voorkeuren", "Preferences"); + const MessagePreferences = language.key(localization.MessagePreferences); return ( <> diff --git a/starsky/starsky/clientapp/src/containers/search.tsx b/starsky/starsky/clientapp/src/containers/search.tsx index 73edfe22ec..e6d706e84d 100644 --- a/starsky/starsky/clientapp/src/containers/search.tsx +++ b/starsky/starsky/clientapp/src/containers/search.tsx @@ -6,6 +6,7 @@ import ArchiveSidebar from "../components/organisms/archive-sidebar/archive-side import useGlobalSettings from "../hooks/use-global-settings"; import useLocation from "../hooks/use-location/use-location"; import { IArchiveProps } from "../interfaces/IArchiveProps"; +import localization from "../localization/localization.json"; import { Language } from "../shared/language"; import { URLPath } from "../shared/url-path"; import MenuMenuSearchContainer from "./menu-search-container/menu-search-container"; @@ -14,13 +15,10 @@ function Search(archive: Readonly) { // Content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageNumberOfResults = language.text("resultaten", "results"); - const MessageNoResult = language.text("Geen resultaat", "No result"); - const MessageTryOtherQuery = language.text( - "Probeer een andere zoekopdracht", - "Try another search query" - ); - const MessagePageNumberToken = language.text("Pagina {pageNumber} van ", "Page {pageNumber} of "); // space at end + const MessageNumberOfResults = language.key(localization.MessageNumberOfResults); + const MessageNoResult = language.key(localization.MessageNoResult); + const MessageTryOtherQuery = language.key(localization.MessageTryOtherQuery); + const MessagePageNumberToken = language.key(localization.MessagePageNumberToken); // space at end const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/containers/trash.tsx b/starsky/starsky/clientapp/src/containers/trash.tsx index 332d07e878..9b9eee0e9c 100644 --- a/starsky/starsky/clientapp/src/containers/trash.tsx +++ b/starsky/starsky/clientapp/src/containers/trash.tsx @@ -7,6 +7,7 @@ import { ArchiveContext, defaultStateFallback } from "../contexts/archive-contex import useGlobalSettings from "../hooks/use-global-settings"; import useLocation from "../hooks/use-location/use-location"; import { IArchiveProps } from "../interfaces/IArchiveProps"; +import localization from "../localization/localization.json"; import { Language } from "../shared/language"; import { URLPath } from "../shared/url-path"; @@ -14,12 +15,10 @@ function Trash(archive: Readonly) { // Content const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageEmptyTrash = language.text( - "Er staat niets in de prullenmand", - "There is nothing in the trash" - ); - const MessageNumberOfResults = language.text("resultaten", "results"); - const MessageNoResult = language.text("Geen resultaat", "No result"); + + const MessageNumberOfResults = language.key(localization.MessageNumberOfResults); + const MessageNoResult = language.key(localization.MessageNoResult); + const MessageEmptyTrash = language.key(localization.MessageEmptyTrash); const history = useLocation(); diff --git a/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.spec.tsx b/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.spec.tsx index 14cd47d7ce..84992b363c 100644 --- a/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.spec.tsx +++ b/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.spec.tsx @@ -1,6 +1,6 @@ import { render } from "@testing-library/react"; import React from "react"; -import * as Archive from "../containers/archive"; +import * as Archive from "../containers/archive/archive"; import * as Login from "../containers/login"; import * as Search from "../containers/search"; import * as Trash from "../containers/trash"; diff --git a/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.tsx b/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.tsx index a1aa0f5f69..35d159e796 100644 --- a/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.tsx +++ b/starsky/starsky/clientapp/src/contexts-wrappers/archive-wrapper.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; import Preloader from "../components/atoms/preloader/preloader"; -import Archive from "../containers/archive"; +import Archive from "../containers/archive/archive"; import { Login } from "../containers/login"; import Search from "../containers/search"; import Trash from "../containers/trash"; diff --git a/starsky/starsky/clientapp/src/hooks/use-global-settings.spec.ts b/starsky/starsky/clientapp/src/hooks/use-global-settings.spec.ts index b8eb372fca..762f7c3040 100644 --- a/starsky/starsky/clientapp/src/hooks/use-global-settings.spec.ts +++ b/starsky/starsky/clientapp/src/hooks/use-global-settings.spec.ts @@ -1,6 +1,6 @@ import { SupportedLanguages } from "../shared/language"; -import useGlobalSettings, { IGlobalSettings } from "./use-global-settings"; import { mountReactHook } from "./___tests___/test-hook"; +import useGlobalSettings, { IGlobalSettings } from "./use-global-settings"; describe("useGlobalSettings", () => { describe("language", () => { @@ -43,5 +43,68 @@ describe("useGlobalSettings", () => { expect(hook.language).toBe(SupportedLanguages.nl); }); + + it("get german language de", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-AT", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-AT"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-BE", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-BE"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-CH", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-CH"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-IT", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-IT"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-LI", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-LI"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); + + it("get german language de-LU", () => { + const languageGetter = jest.spyOn(window.navigator, "language", "get"); + languageGetter.mockReturnValue("de-LU"); + + runHook(); + + expect(hook.language).toBe(SupportedLanguages.de); + }); }); }); diff --git a/starsky/starsky/clientapp/src/hooks/use-global-settings.ts b/starsky/starsky/clientapp/src/hooks/use-global-settings.ts index 2135a9088d..38f09651cb 100644 --- a/starsky/starsky/clientapp/src/hooks/use-global-settings.ts +++ b/starsky/starsky/clientapp/src/hooks/use-global-settings.ts @@ -16,6 +16,22 @@ const useGlobalSettings = (): IGlobalSettings => { return SupportedLanguages.nl; case "nl": return SupportedLanguages.nl; + case "de": + return SupportedLanguages.de; + case "de-de": + return SupportedLanguages.de; + case "de-at": + return SupportedLanguages.de; + case "de-be": + return SupportedLanguages.de; + case "de-ch": + return SupportedLanguages.de; + case "de-it": + return SupportedLanguages.de; + case "de-li": + return SupportedLanguages.de; + case "de-lu": + return SupportedLanguages.de; default: return SupportedLanguages.en; } diff --git a/starsky/starsky/clientapp/src/interfaces/IAppSettings.ts b/starsky/starsky/clientapp/src/interfaces/IAppSettings.ts index 7664542127..1e87e498f6 100644 --- a/starsky/starsky/clientapp/src/interfaces/IAppSettings.ts +++ b/starsky/starsky/clientapp/src/interfaces/IAppSettings.ts @@ -1,5 +1,11 @@ +import { IAppSettingsDefaultEditorApplication } from "./IAppSettingsDefaultEditorApplication"; +import { RawJpegMode } from "./ICollectionsOpenType"; + export interface IAppSettings { verbose: boolean; storageFolder: string; storageFolderAllowEdit: boolean; + useLocalDesktop: boolean; + defaultDesktopEditor: IAppSettingsDefaultEditorApplication[]; + desktopCollectionsOpen: RawJpegMode; } diff --git a/starsky/starsky/clientapp/src/interfaces/IAppSettingsDefaultEditorApplication.ts b/starsky/starsky/clientapp/src/interfaces/IAppSettingsDefaultEditorApplication.ts new file mode 100644 index 0000000000..b7ca323e34 --- /dev/null +++ b/starsky/starsky/clientapp/src/interfaces/IAppSettingsDefaultEditorApplication.ts @@ -0,0 +1,6 @@ +import { ImageFormat } from "./IFileIndexItem"; + +export interface IAppSettingsDefaultEditorApplication { + applicationPath: string; + imageFormats: ImageFormat[]; +} diff --git a/starsky/starsky/clientapp/src/interfaces/ICollectionsOpenType.ts b/starsky/starsky/clientapp/src/interfaces/ICollectionsOpenType.ts new file mode 100644 index 0000000000..c50afb392d --- /dev/null +++ b/starsky/starsky/clientapp/src/interfaces/ICollectionsOpenType.ts @@ -0,0 +1,5 @@ +export enum RawJpegMode { + Default = 0, + Jpeg = 1, + Raw = 2 +} diff --git a/starsky/starsky/clientapp/src/interfaces/IEnvFeatures.ts b/starsky/starsky/clientapp/src/interfaces/IEnvFeatures.ts index 00042dcdaf..647717e375 100644 --- a/starsky/starsky/clientapp/src/interfaces/IEnvFeatures.ts +++ b/starsky/starsky/clientapp/src/interfaces/IEnvFeatures.ts @@ -1,4 +1,5 @@ export interface IEnvFeatures { systemTrashEnabled: boolean; - useLocalDesktopUi: boolean; + useLocalDesktop: boolean; + openEditorEnabled: boolean; } diff --git a/starsky/starsky/clientapp/src/interfaces/IFileIndexItem.ts b/starsky/starsky/clientapp/src/interfaces/IFileIndexItem.ts index 5b356c7c90..22e07d4990 100644 --- a/starsky/starsky/clientapp/src/interfaces/IFileIndexItem.ts +++ b/starsky/starsky/clientapp/src/interfaces/IFileIndexItem.ts @@ -29,8 +29,8 @@ export interface IFileIndexItem { locationCountryCode?: string; locationCity?: string; locationState?: string; - imageWidth: number; - imageHeight: number; + imageWidth?: number; + imageHeight?: number; size?: number; sidecarExtensionsList?: string[]; collectionPaths?: string[]; diff --git a/starsky/starsky/clientapp/src/interfaces/ILanguageLocalization.ts b/starsky/starsky/clientapp/src/interfaces/ILanguageLocalization.ts new file mode 100644 index 0000000000..aa1c1418c1 --- /dev/null +++ b/starsky/starsky/clientapp/src/interfaces/ILanguageLocalization.ts @@ -0,0 +1,11 @@ +export interface ILanguageLocalization { + nl: string; + en: string; + de: string; +} + +export const LanguageLocalizationExample: ILanguageLocalization = { + nl: "Nederlands", + en: "English", + de: "Deutsch" +}; diff --git a/starsky/starsky/clientapp/src/localization/localization.json b/starsky/starsky/clientapp/src/localization/localization.json index 8b3b9ff07e..29a2187343 100644 --- a/starsky/starsky/clientapp/src/localization/localization.json +++ b/starsky/starsky/clientapp/src/localization/localization.json @@ -1,246 +1,1002 @@ { "MessageMoveFolderIntoTrashIntroText": { "en": "Are you sure you want to move this folder into the trash?", - "nl": "Weet u zeker dat u deze map naar de prullenbak wilt verplaatsen?" + "nl": "Weet u zeker dat u deze map naar de prullenbak wilt verplaatsen?", + "de": "Möchten Sie diesen Ordner wirklich in den Papierkorb verschieben?" }, "MessageCancel": { "en": "Cancel", - "nl": "Annuleren" + "nl": "Annuleren", + "de": "Abbrechen" }, "MessageTrash": { "en": "Trash", - "nl": "Prullenbak" + "nl": "Prullenbak", + "de": "Papierkorb" }, "MessageMoveToTrash": { "en": "Move to trash", - "nl": "Verplaatsen naar prullenbak" + "nl": "Verplaatsen naar prullenbak", + "de": "In den Papierkorb verschieben" }, "MessageMoveCurrentFolderToTrash": { "en": "Move current folder to trash", - "nl": "Huidige map naar prullenbak" + "nl": "Huidige map naar prullenbak", + "de": "Aktuellen Ordner in den Papierkorb verschieben" }, "MessageMove": { "en": "Move", - "nl": "Verplaatsen" + "nl": "Verplaatsen", + "de": "Verschieben" }, "MessageTo": { "en": "to", - "nl": "naar" + "nl": "naar", + "de": "in" }, "MessageRestoreFromTrash": { "en": "Restore from trash", - "nl": "Herstellen uit prullenbak" + "nl": "Herstellen uit prullenbak", + "de": "Aus dem Papierkorb wiederherstellen" }, "MessageSynchronizeManually": { "en": "Synchronize manually", - "nl": "Handmatig synchroniseren" + "nl": "Handmatig synchroniseren", + "de": "Manuell synchronisieren" }, "MessageDownload": { "en": "Download", - "nl": "Download" + "nl": "Download", + "de": "Herunterladen" }, "MessagePublish": { "en": "Publish", - "nl": "Publiceren" + "nl": "Publiceren", + "de": "Veröffentlichen" }, "MessageReadOnlyFile": { "en": "Read only file", - "nl": "Alleen-lezen bestand" + "nl": "Alleen-lezen bestand", + "de": "Schreibgeschützte Datei" }, "MessageNotFoundSourceMissing": { "en": "Not found. Source missing.", - "nl": "Niet gevonden. Bron ontbreekt." + "nl": "Niet gevonden. Bron ontbreekt.", + "de": "Nicht gefunden. Quelle fehlt." }, "MessageServerInputError": { "en": "Something is wrong with the input", - "nl": "Er is iets mis met de input" + "nl": "Er is iets mis met de input", + "de": "Etwas stimmt nicht mit der Eingabe" }, "MessageIsInTheTrash": { "en": "Is in the trash", - "nl": "Staat in de prullenbak" + "nl": "Staat in de prullenbak", + "de": "Befindet sich im Papierkorb" }, "MessageDeletedRestoreInstruction": { "en": "'Restore from Trash' to edit the item", - "nl": "'Zet terug uit prullenbak' om het item te bewerken" + "nl": "'Zet terug uit prullenbak' om het item te bewerken", + "de": "'Aus dem Papierkorb wiederherstellen', um das Element zu bearbeiten" }, "MessageHome": { "en": "Home", - "nl": "Home" + "nl": "Home", + "de": "Startseite" }, "MessagePhotosOfThisWeek": { "en": "Photos of this week", - "nl": "Foto's van deze week" + "nl": "Foto's van deze week", + "de": "Fotos dieser Woche" }, "MessageImport": { "en": "Import", - "nl": "Importeren" + "nl": "Importeren", + "de": "Import" }, "MessagePreferences": { "en": "Preferences", - "nl": "Voorkeuren" + "nl": "Voorkeuren", + "de": "Einstellungen" }, "MessageLogout": { "en": "Logout", - "nl": "Uitloggen" + "nl": "Uitloggen", + "de": "Ausloggen" }, "MessageSaved": { "en": "Saved", - "nl": "Opgeslagen" + "nl": "Opgeslagen", + "de": "Gespeichert" }, "MessageImportHeader": { "en": "Import", - "nl": "Importeren" + "nl": "Importeren", + "de": "Import" }, "MessageCloseDetailScreenDialog": { "en": "Close detail screen", - "nl": "Sluit detailscherm" + "nl": "Sluit detailscherm", + "de": "Detailbildschirm schließen" }, "MessageCloseDialogBackToFolder": { "en": "Parent folder", - "nl": "Terug naar map" + "nl": "Terug naar map", + "de": "Elternordner" }, "MessageIncludingColonWord": { "en": "Including: ", - "nl": "Inclusief: " + "nl": "Inclusief: ", + "de": "Inklusive: " }, "MessageAddLocation": { "en": "Add location", - "nl": "Voeg locatie toe" + "nl": "Voeg locatie toe", + "de": "Ort hinzufügen" }, "MessageUpdateLocation": { "en": "Update location", - "nl": "Werk locatie bij" + "nl": "Werk locatie bij", + "de": "Ort aktualisieren" }, "MessageViewLocation": { "en": "View location", - "nl": "Bekijk locatie" + "nl": "Bekijk locatie", + "de": "Ort anzeigen" }, "MessageErrorGenericFail": { "en": "Something went wrong with the update. Please try again", - "nl": "Er is iets misgegaan met het updaten. Probeer het opnieuw" + "nl": "Er is iets misgegaan met het updaten. Probeer het opnieuw", + "de": "Bei der Aktualisierung ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut" }, "MessageNoPhotosInFolder": { "en": "There are no photos in this folder", - "nl": "Er zijn geen foto's in deze map" + "nl": "Er zijn geen foto's in deze map", + "de": "In diesem Ordner befinden sich keine Fotos" }, "MessageNewUserNoPhotosInFolder": { "en": "New? Set your drive location in the settings.", - "nl": "Nieuw? Stel je schijflocatie in de instellingen." + "nl": "Nieuw? Stel je schijflocatie in de instellingen.", + "de": "Neu? Legen Sie Ihren Laufwerksspeicherort in den Einstellungen fest." }, "MessageItemsOutsideFilter": { "en": "There are more items, but these are outside of your filters. To see everything click on 'Reset Filter'", - "nl": "Er zijn meer items, maar deze vallen buiten je filters. Om alles te zien klik op 'Herstel Filter'" + "nl": "Er zijn meer items, maar deze vallen buiten je filters. Om alles te zien klik op 'Herstel Filter'", + "de": "Es gibt mehr Elemente, aber diese befinden sich außerhalb Ihrer Filter. Um alles zu sehen, klicken Sie auf 'Filter zurücksetzen'" }, "MessageSelectPresentPerfect": { "en": "selected", - "nl": "geselecteerd" + "nl": "geselecteerd", + "de": "ausgewählt" }, "MessageSelectAction": { "en": "Select", - "nl": "Selecteer" + "nl": "Selecteer", + "de": "Auswählen" }, "MessageNoneSelected": { "en": "Nothing selected", - "nl": "Niets geselecteerd" + "nl": "Niets geselecteerd", + "de": "Nichts ausgewählt" }, "MessageUndoSelection": { "en": "Undo selection", - "nl": "Undo selectie" + "nl": "Undo selectie", + "de": "Auswahl rückgängig machen" }, "MessageSelectAll": { "en": "Select all", - "nl": "Alles selecteren" + "nl": "Alles selecteren", + "de": "Alles auswählen" + }, + "MessageAllName": { + "en": "All", + "nl": "Alles", + "de": "Alles" }, "MessageSelectFurther": { "en": "Select further", - "nl": "Verder selecteren" + "nl": "Verder selecteren", + "de": "Weiter auswählen" }, "MessageGoToParentFolder": { "en": "Go to parent folder", - "nl": "Ga naar bovenliggende map" + "nl": "Ga naar bovenliggende map", + "de": "Zum übergeordneten Ordner gehen" }, "MessageMkdir": { "en": "Create folder", - "nl": "Map maken" - }, - "MessageDisplayOptions": { - "en": "Display options", - "nl": "Weergave opties" + "nl": "Map maken", + "de": "Ordner erstellen" }, "MessageRenameDir": { "en": "Rename", - "nl": "Naam wijzigen" + "nl": "Naam wijzigen", + "de": "Umbenennen" }, "MessageLabels": { "en": "Labels", - "nl": "Labels" + "nl": "Labels", + "de": "Etiketten" }, "MessageRenameFileName": { "en": "Rename file name", - "nl": "Bestandsnaam wijzigen" + "nl": "Bestandsnaam wijzigen", + "de": "Dateinamen umbenennen" }, "MessageRotateToRight": { "en": "Rotation to the right", - "nl": "Rotatie naar rechts" + "nl": "Rotatie naar rechts", + "de": "Drehung nach rechts" }, "MessageDeleteImmediately": { "en": "Delete immediately", - "nl": "Verwijder onmiddellijk" + "nl": "Verwijder onmiddellijk", + "de": "Unmittelbar löschen" }, "MessageVideoPlayBackError": { "en": "There is something wrong with the playback of this video. Try 'More' and 'Download'. ", - "nl": "Er is iets mis met het afspelen van deze video. Probeer eens via het menu 'Meer' en 'Download'." + "nl": "Er is iets mis met het afspelen van deze video. Probeer eens via het menu 'Meer' en 'Download'.", + "de": "Beim Abspielen dieses Videos ist etwas schiefgelaufen. Versuchen Sie 'Weitere' und 'Download'." }, "MessageVideoNotFound": { "en": "This video is not found", - "nl": "Deze video is niet gevonden" + "nl": "Deze video is niet gevonden", + "de": "Dieses Video wurde nicht gefunden" + }, + "MessageColorClassIsUpdated": { + "en": "Colorclass is updated", + "nl": "Colorclass is bijgewerkt", + "de": "Farbklasse wurde aktualisiert" }, "ColorClassColour0": { "en": "Colorless", - "nl": "Kleurloos" + "nl": "Kleurloos", + "de": "Farblos" }, "ColorClassColour1": { "en": "Pink", - "nl": "Roze" + "nl": "Roze", + "de": "Rosa" }, "ColorClassColour2": { "en": "Red", - "nl": "Rood" + "nl": "Rood", + "de": "Rot" }, "ColorClassColour3": { "en": "Orange", - "nl": "Oranje" + "nl": "Oranje", + "de": "Orange" }, "ColorClassColour4": { "en": "Yellow", - "nl": "Geel" + "nl": "Geel", + "de": "Gelb" }, "ColorClassColour5": { "en": "Green", - "nl": "Groen" + "nl": "Groen", + "de": "Grün" }, "ColorClassColour6": { "en": "Azure", - "nl": "Azuur" + "nl": "Azuur", + "de": "Azurblau" }, "ColorClassColour7": { "en": "Blue", - "nl": "Blauw" + "nl": "Blauw", + "de": "Blau" }, "ColorClassColour8": { "en": "Grey", - "nl": "Grijs" + "nl": "Grijs", + "de": "Grau" }, "ColorClassColourResetFilter": { "en": "Reset filter", - "nl": "Herstel filter" + "nl": "Herstel filter", + "de": "Filter zurücksetzen" }, "MessageCloseDialog": { "en": "Close", - "nl": "Sluiten" + "nl": "Sluiten", + "de": "Schließen" + }, + "MessageDesktopEditorOpenSingleFile": { + "en": "Open file", + "nl": "Open bestand", + "de": "Datei öffnen" + }, + "MessageDesktopEditorOpenMultipleFiles": { + "en": "Open files", + "nl": "Open bestanden", + "de": "Dateien öffnen" + }, + "MessageDesktopEditorUnableToOpen": { + "en": "Sorry, something went wrong opening the file", + "nl": "Sorry, er iets misgegaan met het openen van het bestand", + "de": "Entschuldigung, beim Öffnen der Datei ist ein Fehler aufgetreten" + }, + "MessageDesktopEditorConfirmationHeading": { + "en": "Do you really want to edit all of the selected photos?", + "nl": "Wil je echt alle geselecteerde foto's bewerken?", + "de": "Möchten Sie wirklich alle ausgewählten Fotos bearbeiten?" + }, + "MessageDesktopEditorConfirmationIntroText": { + "en": "This prompt is to prevent potential issues such as your computer freezing", + "nl": "Deze melding is bedoeld om te voorkomen dat er eventuele problemen optreden, zoals het vastlopen van je computer.", + "de": "Dieser Hinweis soll potenzielle Probleme wie das Einfrieren Ihres Computers verhindern" + }, + "MessageItemSelectionRequired": { + "en": "Please select at least one item before proceeding.", + "nl": "Selecteer alstublieft eerst minstens één item voordat u doorgaat.", + "de": "Bitte wählen Sie mindestens einen Artikel aus, bevor Sie fortfahren." + }, + "MessageAppSettingsEntireAppScope": { + "en": "The AppSettings may only be modified by Administrators. These settings are applied for the entire application.", + "nl": "De AppSettings mogen alleen worden aangepast door Administrators. Deze instellingen worden toegepast voor de gehele applicatie.", + "de": "Die AppSettings dürfen nur von Administratoren geändert werden. Diese Einstellungen gelten für die gesamte Anwendung." + }, + "MessageChangeNeedReSync": { + "en": "You have changed this setting, now you need to perform a full sync. Go to the root folder, the more menu and click on manual sync.", + "nl": "Je hebt deze instelling veranderd, nu dien je een volledige sync uit te voeren. Ga naar de hoofdmap, het meer-menu en klik op handmatig synchroniseren.", + "de": "Sie haben diese Einstellung geändert, jetzt müssen Sie eine vollständige Synchronisierung durchführen. Gehen Sie zum Stammverzeichnis, zum Menü 'Weitere Optionen' und klicken Sie auf manuelle Synchronisierung." + }, + "MessageAppSettingsStorageFolder": { + "en": "Storage Folder", + "nl": "Opslag map", + "de": "Speicherordner" + }, + "MessageAppSettingStorageFolderSaveFail": { + "en": "Apologies, unable to save, we couldn't find the directory", + "nl": "Sorry het opslaan van de map is niet gelukt, de map is niet gevonden", + "de": "Entschuldigung, Speichern nicht möglich, Verzeichnis konnte nicht gefunden werden" + }, + "MessageAppSettingStorageFolderEnvUsedFail": { + "en": "Please update the 'app__storageFolder' environment variable to change the location", + "nl": "Update alsjeblieft de 'app__storageFolder' environment variable om de opslag locatie te veranderen", + "de": "Bitte aktualisieren Sie die Umgebungsvariable 'app__storageFolder', um den Speicherort zu ändern" + }, + "MessageSwitchButtonDesktopCollectionsRawOn": { + "en": "Raw first", + "nl": "Raw eerst", + "de": "Raw zuerst" + }, + "MessageSwitchButtonDesktopCollectionsJpegDefaultOff": { + "en": "Jpeg first", + "nl": "Jpeg eerst", + "de": "Jpeg zuerst" + }, + "MessageSwitchButtonDesktopApplication": { + "en": "AppSettings: Desktop application", + "nl": "AppSettings: Desktop applicatie", + "de": "App-Einstellungen: Desktop-Anwendung" + }, + "MessageSwitchButtonDesktopApplicationDescription": { + "en": "These settings are only available when using as Desktop application", + "nl": "Deze instellingen zijn alleen beschikbaar als desktop applicatie", + "de": "Diese Einstellungen sind nur verfügbar, wenn sie als Desktop-Anwendung verwendet werden" + }, + "MessageAppSettingDefaultEditorPhotos": { + "en": "Default application to edit photos:", + "nl": "Standaardtoepassing om foto's te bewerken:", + "de": "Standardanwendung zum Bearbeiten von Fotos:" + }, + "MessageAppSettingDefaultEditorPhotosDescription": { + "en": "Keep emthy to use the default system application ", + "nl": "Hou leeg om de standaardtoepassing van het systeem te gebruiken", + "de": "Leer lassen, um die Standard-Systemanwendung zu verwenden" + }, + "MessageSwitchButtonDesktopCollectionsUpdateError": { + "en": "Something went wrong updating the collection preferences", + "nl": "Er ging iets met het updaten van de collectie voorkeur ", + "de": "Beim Aktualisieren der Sammlungseinstellungen ist ein Fehler aufgetreten" + }, + "MessageSwitchButtonDesktopCollectionsUpdateSuccess": { + "en": "The collection preference is updated", + "nl": "De collection voorkeur is bijgewerkt", + "de": "Die Sammlungseinstellung wurde aktualisiert" + }, + "MessageSwitchButtonDesktopCollectionsRawJpegUpdateError": { + "en": "Something went wrong updating the Raw / Jpeg preferences", + "nl": "Er ging iets mis bij het bijwerken van de voorkeur voor Raw / Jpeg", + "de": "Beim Aktualisieren der Raw / Jpeg-Einstellungen ist ein Fehler aufgetreten" + }, + "MessageSwitchButtonDesktopCollectionsRawJpegUpdateSuccess": { + "en": "The Raw / Jpeg preference is updated", + "nl": "De voorkeur voor Raw / Jpeg is bijgewerkt", + "de": "Die Raw / Jpeg-Einstellung wurde aktualisiert" + }, + "MessageUnknownUsername": { + "en": "Unknown username", + "nl": "Onbekende gebruikersnaam", + "de": "Unbekannter Benutzername" + }, + "MessageUsername": { + "en": "Username", + "nl": "Gebruikersnaam", + "de": "Benutzername" + }, + "MessageRole": { + "en": "Role", + "nl": "Rol", + "de": "Rolle" + }, + "MessageExamplePassword": { + "en": "supersafe", + "nl": "superveilig", + "de": "supersicher" + }, + "MessageCurrentPassword": { + "en": "Enter your current password", + "nl": "Voer uw huidige wachtwoord in", + "de": "Geben Sie Ihr aktuelles Passwort ein" + }, + "MessageChangedPassword": { + "en": "Enter your new password", + "nl": "Voer uw nieuwe wachtwoord in", + "de": "Geben Sie Ihr neues Passwort ein" + }, + "MessageChangedConfirmPassword": { + "en": "And your new password again", + "nl": "Herhaal je nieuwe wachtwoord", + "de": "Und Ihr neues Passwort erneut" + }, + "MessageNoPassword": { + "en": "Enter the current and new password", + "nl": "Voer het huidige en nieuwe wachtwoord in", + "de": "Geben Sie das aktuelle und das neue Passwort ein" + }, + "MessagePassword": { + "en": "Enter your password", + "nl": "Geef je wachtwoord op", + "de": "Geben Sie Ihr Passwort ein" + }, + "MessagePasswordChanged": { + "en": "Your password has been successfully changed", + "nl": "Je wachtwoord is succesvol veranderd", + "de": "Ihr Passwort wurde erfolgreich geändert" + }, + "MessagePasswordNoMatch": { + "en": "The passwords do not match", + "nl": "De wachtwoorden komen niet overeen", + "de": "Die Passwörter stimmen nicht überein" + }, + "MessagePasswordModalError": { + "en": "The new password does not meet the criteria", + "nl": "Het nieuwe wachtwoord voldoet niet aan de criteria", + "de": "Das neue Passwort entspricht nicht den Kriterien" + }, + "MessageCreateNewAccount": { + "en": "Create new account", + "nl": "Maak nieuw account", + "de": "Neues Konto erstellen" + }, + "MessageExampleUsername": { + "en": "dont@mail.me", + "nl": "dont@mail.me", + "de": "dont@mail.me" + }, + "MessageConfirmPassword": { + "en": "Enter your password again", + "nl": "Vul je wachtwoord nog een keer in", + "de": "Geben Sie Ihr Passwort erneut ein" + }, + "MessageNoUsernamePassword": { + "en": "Enter an email address and password", + "nl": "Voer een emailadres en een wachtwoord in", + "de": "Geben Sie eine E-Mail-Adresse und ein Passwort ein" + }, + "MessageWrongFormatEmailAddress": { + "en": "Check your email address", + "nl": "Controleer je email adres", + "de": "Überprüfen Sie Ihre E-Mail-Adresse" + }, + "MessagePasswordToShort": { + "en": "Use at least 8 characters for your password", + "nl": "Gebruik minimaal 8 tekens voor je wachtwoord", + "de": "Verwenden Sie mindestens 8 Zeichen für Ihr Passwort" + }, + "MessageRejectedBadRequest": { + "en": "This request was rejected because the security requirements were not met (Error 400)", + "nl": "Dit verzoek is afgewezen aangezien er niet voldaan is aan de beveiligingseisen (Error 400)", + "de": "Dieses Anfrage wurde abgelehnt, weil die Sicherheitsanforderungen nicht erfüllt wurden (Fehler 400)" + }, + "MessageRegistrationTurnedOff": { + "en": "Registration is turned off", + "nl": "Registratie is uitgezet", + "de": "Die Registrierung ist deaktiviert" + }, + "MessageSignInInstead": { + "en": "Sign in instead", + "nl": "In plaats daarvan inloggen", + "de": "Stattdessen anmelden" + }, + "MessageLegalCreateAccountHtml": { + "en": "By creating an account you agree to Starsky's Conditions of Use. Please see our Privacy Notice and our Cookies Notice", + "nl": "Door het creëren van een account gaat u akkoord met de Algemene Voorwaarden van Starsky. Raadpleeg en bekijk hier onze Privacykennisgeving en onze Cookieverklaring.", + "de": "Durch das Erstellen eines Kontos stimmen Sie den Allgemeinen Geschäftsbedingungen von Starsky zu. Bitte beachten Sie unsere Datenschutzerklärung und unsere Cookie-Richtlinie." + }, + "MessageFieldMaxLength": { + "en": "The field below can have a maximum of {maxlength} characters", + "nl": "Het onderstaande veld mag maximaal {maxlength} tekens hebben", + "de": "Das Feld unten kann maximal {maxlength} Zeichen enthalten" + }, + "MessageWrongUsernamePassword": { + "en": "Your username or password is incorrect. Try again", + "nl": "Je gebruikersnaam of wachtwoord is niet juist. Probeer het opnieuw", + "de": "Ihr Benutzername oder Ihr Passwort ist falsch. Bitte versuchen Sie es erneut." + }, + "MessageLockedOut": { + "en": "You've tried to login too many times, please try again in an hour", + "nl": "Je hebt te vaak geprobeerd in te loggen, probeer het over een uur nog een keer", + "de": "Sie haben zu oft versucht, sich anzumelden. Bitte versuchen Sie es in einer Stunde erneut." + }, + "MessageConnection": { + "en": "No connection is possible, please try again later", + "nl": "Er is geen verbinding mogelijk, probeer het later opnieuw", + "de": "Es ist keine Verbindung möglich, bitte versuchen Sie es später erneut." + }, + "MessageDatabaseConnection": { + "en": "There are database connection issues. Check and edit the appsettings", + "nl": "Er zijn problemen met de verbinding met de database. Controleer en pas de appsettings aan", + "de": "Es gibt Probleme mit der Verbindung zur Datenbank. Überprüfen und bearbeiten Sie die App-Einstellungen." + }, + "LogoutWarning": { + "en": "Do you want to log out?", + "nl": "Wil je uitloggen?", + "de": "Möchten Sie sich abmelden?" + }, + "MessageStayLoggedIn": { + "en": "Stay logged in", + "nl": "Blijf ingelogd", + "de": "Angemeldet bleiben" + }, + "MessageLogin": { + "en": "Login", + "nl": "Inloggen", + "de": "Anmelden" + }, + "MessageCreateAccount": { + "en": "Create account", + "nl": "Account maken", + "de": "Konto erstellen" + }, + "MessageMore": { + "en": "More", + "nl": "Meer", + "de": "Mehr" + }, + "MessagePrevious": { + "en": "Previous", + "nl": "Vorige", + "de": "Vorherige" + }, + "MessageNext": { + "en": "Next", + "nl": "Volgende", + "de": "Nächste" + }, + "MessageAddName": { + "en": "Hinzufügen zu", + "nl": "Toevoegen", + "de": "Mehr" + }, + "MessageOverwriteName": { + "en": "Overwrite", + "nl": "Overschrijven", + "de": "Überschreiben" + }, + "MessageTitleName": { + "en": "Title", + "nl": "Titel", + "de": "Titel" + }, + "MessageInfoName": { + "en": "Info", + "nl": "Info", + "de": "Info" + }, + "MessageWriteErrorReadOnly": { + "en": "One or more files are read only. Only the files with write permissions have been updated.", + "nl": "Eén of meerdere bestanden zijn alleen lezen. Alleen de bestanden met schrijfrechten zijn geupdate.", + "de": "Eine oder mehrere Dateien sind schreibgeschützt. Es wurden nur die Dateien mit Schreibrechten aktualisiert." + }, + "MessageErrorNotFoundSourceMissingRunSync": { + "en": "One or more files are already gone. Only the files that are present are updated. Run a manual sync", + "nl": "Eén of meerdere bestanden zijn al verdwenen. Alleen de bestanden die wel aanwezig zijn geupdate. Draai een handmatige sync", + "de": "Eine oder mehrere Dateien sind bereits verschwunden. Es werden nur die vorhandenen Dateien aktualisiert. Führen Sie eine manuelle Synchronisierung durch" + }, + "MessageSearchAndReplaceNameLong": { + "en": "Search and replace", + "nl": "Zoeken en vervangen", + "de": "Suchen und ersetzen" + }, + "MessageSearchAndReplaceNameShort": { + "en": "Replace", + "nl": "Vervangen", + "de": "Ersetzen" + }, + "MessageModifyName": { + "en": "Modify", + "nl": "Wijzigen", + "de": "Ändern" + }, + "MessageForceSyncCurrentFolder": { + "en": "Synchronize current directory manually", + "nl": "Handmatig synchroniseren van huidige map", + "de": "Aktuelles Verzeichnis manuell synchronisieren" + }, + "MessageWhereToFindReleaseReleasesUrlTokenHtml": { + "en": " {releasesToken}", + "nl": " {releasesToken}", + "de": " {releasesToken}" + }, + "MessageWhereToFindReleaseReleasesUrlTokenContent": { + "en": "Go to the release overview", + "nl": "Ga naar het release overzicht", + "de": "Gehen Sie zur Release-Übersicht" + }, + "WhereToFindReleaseElectronApp": { + "en": "Go to the Help menu and then release overview", + "nl": "Ga naar het Help menu en dan release overzicht", + "de": "Gehen Sie zum Hilfemenü und dann zur Release-Übersicht" + }, + "MessageNewVersionUpdateToken": { + "en": "A new version is available {WhereToFindRelease}", + "nl": "Er is een nieuwe versie beschikbaar {WhereToFindRelease}", + "de": "Eine neue Version ist verfügbar {WhereToFindRelease}" + }, + "MessageHealthStatusCriticalErrorsWithTheFollowingComponents": { + "en": "There are critical errors in the following components:", + "nl": "Er zijn kritieke fouten in de volgende onderdelen:", + "de": "In den folgenden Komponenten liegen kritische Fehler vor:" + }, + "MessageNoPhotos": { + "en": "There are no pictures", + "nl": "Er zijn geen foto's", + "de": "Es gibt keine Bilder" + }, + "MessageFilesAdded": { + "en": "These files have been added", + "nl": "Deze bestanden zijn toegevoegd", + "de": "Diese Dateien wurden hinzugefügt" + }, + "MessageApplicationException": { + "en": "We have a disruption on the application right now", + "nl": "We hebben een op dit moment een verstoring op de applicatie", + "de": "Wir haben derzeit eine Störung bei der Anwendung" + }, + "MessageRefreshPageTryAgain": { + "en": "Please reload the application to try again", + "nl": "Herlaad de applicatie om het opnieuw te proberen", + "de": "Laden Sie die Anwendung herunter, um die Option zu testen" + }, + "MessagePublishSelection": { + "en": "Publish selection", + "nl": "Publiceer selectie", + "de": "Auswahl veröffentlichen" + }, + "MessageGenericExportFail": { + "en": "Something went wrong with exporting", + "nl": "Er is iets misgegaan met exporteren", + "de": "Beim Exportieren ist ein Fehler aufgetreten" + }, + "MessageRetryExportFail": { + "en": "Retry this", + "nl": "Probeer het opnieuw", + "de": "Dies erneut versuchen" + }, + "MessageExportReady": { + "en": "The file {createZipKey} has finished exporting.", + "nl": "Het bestand {createZipKey} is klaar met exporteren.", + "de": "Die Datei {createZipKey} wurde erfolgreich exportiert." + }, + "MessageDownloadAsZipArchive": { + "en": "Download as a zip archive", + "nl": "Download als zip-archief", + "de": "Als ZIP-Archiv herunterladen" + }, + "MessageOneMomentPlease": { + "en": "One moment please", + "nl": "Een moment geduld alstublieft", + "de": "Einen Moment bitte" + }, + "MessageItemName": { + "en": "What is the item about?", + "nl": "Waar gaat het item over?", + "de": "Worum geht es bei dem Element?" + }, + "MessageItemNameInUse": { + "en": "This name is already in use, please choose another name", + "nl": "Deze naam is al in gebruik, kies een andere naam", + "de": "Dieser Name wird bereits verwendet. Bitte wählen Sie einen anderen Namen" + }, + "MessagePublishProfileName": { + "en": "Profile setting", + "nl": "Profiel instelling", + "de": "Profil-Einstellung" + }, + "MessagePublishProfileNamesErrored": { + "en": "Profile setting: {publishProfileNames} contains filepath errors", + "nl": "Profiel instelling: {publishProfileNames} bevat bestand locatie fouten", + "de": "Profil-Einstellung: {publishProfileNames} enthält Dateipfadfehler" + }, + "MessageSelectionName": { + "en": "Selection", + "nl": "Selectie", + "de": "Auswahl" + }, + "MessageReadOnlyFolder": { + "en": "Read only folder", + "nl": "Alleen lezen map", + "de": "Nur-Lese-Ordner" + }, + "MessageUpdateLabels": { + "en": "Update labels", + "nl": "Labels wijzigingen", + "de": "Etiketten aktualisieren" + }, + "MessageColorClassification": { + "en": "Color Classification", + "nl": "Kleur-Classificatie", + "de": "Farbklassifizierung" + }, + "MessageDateTimeAgoEdited": { + "en": "ago edited", + "nl": "geleden bewerkt", + "de": "vorher bearbeitet" + }, + "MessageDateLessThan1Minute": { + "en": "less than one minute", + "nl": "minder dan één minuut", + "de": "weniger als eine Minute" + }, + "MessageDateMinutes": { + "en": "minutes", + "nl": "minuten", + "de": "Minuten" + }, + "MessageDateHour": { + "en": "hour", + "nl": "uur", + "de": "Stunde" + }, + "MessageCopiedLabels": { + "en": "The labels have been copied", + "nl": "De labels zijn gekopieerd", + "de": "Die Labels wurden kopiert" + }, + "MessagePasteLabels": { + "en": "The labels have been overwritten", + "nl": "De labels zijn overschreven", + "de": "Die Labels wurden überschrieben" + }, + "MessageCreationDate": { + "en": "Creation date", + "nl": "Aanmaakdatum", + "de": "Erstellungsdatum" + }, + "MessageCreationDateIsAtUnknownTime": { + "en": "is at an unknown time", + "nl": "is op een onbekend moment", + "de": "ist zu einem unbekannten Zeitpunkt" + }, + "MessageNounNameless": { + "en": "Unnamed", + "nl": "Naamloze", + "de": "Unbenannt" + }, + "MessageNounNone": { + "en": "Not any", + "nl": "Geen enkele", + "de": "Nicht eine" + }, + "MessageLocation": { + "en": "location", + "nl": "locatie", + "de": "Lokalisierung" + }, + "MessageCreateNewFolderFeature": { + "en": "Create new folder", + "nl": "Nieuwe map aanmaken", + "de": "Neuen Ordner erstellen" + }, + "MessageNonValidDirectoryName": { + "en": "Check the name, this folder cannot be created in this way", + "nl": "Controleer de naam, deze map kan niet zo worden aangemaakt", + "de": "Überprüfen Sie den Namen, dieser Ordner kann nicht auf diese Weise erstellt werden" + }, + "MessageGeneralMkdirCreateError": { + "en": "An error occurred while creating this folder", + "nl": "Er is misgegaan met het aanmaken van deze map", + "de": "Beim Erstellen dieses Ordners ist ein Fehler aufgetreten" + }, + "MessageDirectoryExistError": { + "en": "The folder already exists, try a different name", + "nl": "De map bestaat al, probeer een andere naam", + "de": "Der Ordner existiert bereits. Versuchen Sie einen anderen Namen" + }, + "MessageRenameCurrentFolder": { + "en": "Rename current folder", + "nl": "Huidige mapnaam wijzigen", + "de": "Aktuellen Ordner umbenennen" + }, + "MessageRemoveCache": { + "en": "Refresh cache of current directory", + "nl": "Verwijder cache van huidige map", + "de": "Aktualisieren Sie den Cache des aktuellen Verzeichnisses" + }, + "MessageGeoSync": { + "en": "Automatically add geolocation", + "nl": "Voeg geolocatie automatisch toe", + "de": "Geolocation automatisch hinzufügen" + }, + "MessageGeoSyncExplainer": { + "en": "The location is derived from a gpx file located in the current folder and based on the location place names are appended to the images", + "nl": "De locatie wordt afgeleid van een gpx bestand die zich in de huidige map bevind en op basis van de locatie worden er plaatsnamen bij de afbeeldingen gevoegd", + "de": "Der Ort wird aus einer in dem aktuellen Ordner befindlichen gpx-Datei abgeleitet und basierend auf dem Ort werden Ortsnamen den Bildern hinzugefügt" + }, + "MessageManualThumbnailSync": { + "en": "Generate thumbnail images", + "nl": "Thumbnail afbeeldingen generen", + "de": "Miniaturbilder generieren" + }, + "MessageManualThumbnailSyncExplainer": { + "en": "This action generate on the background lots of thumbnail images, this does impact the performance", + "nl": "Deze actie genereert op de achtergrond veel miniatuurafbeeldingen, dit heeft invloed op de prestaties", + "de": "Diese Aktion generiert im Hintergrund viele Miniaturbilder, was sich auf die Leistung auswirkt" + }, + "MessageNonValidExtension": { + "en": "This file cannot be saved", + "nl": "Dit bestand kan zo niet worden weggeschreven", + "de": "Diese Datei kann nicht gespeichert werden" + }, + "MessageChangeToDifferentExtension": { + "en": "Pay attention! You change the file extension, which can make it unreadable", + "nl": "Let op! Je veranderd de extensie van het bestand, deze kan hierdoor onleesbaar worden", + "de": "Achtung! Sie ändern die Dateierweiterung, was dazu führen kann, dass sie nicht lesbar wird" + }, + "MessageRenameServerError": { + "en": "Something went wrong with the request, please try again later", + "nl": "Er is iets misgegaan met de aanvraag, probeer het later opnieuw", + "de": "Bei der Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut" + }, + "MessageDisplayOptions": { + "en": "Display options", + "nl": "Weergave opties", + "de": "Anzeigeoptionen" + }, + "MessageDisplayOptionsSwitchButtonCollectionsOff": { + "en": "Show raw files", + "nl": "Toon raw bestanden", + "de": "Rohdateien anzeigen" + }, + "MessageDisplayOptionsSwitchButtonCollectionsOn": { + "en": "Hide Raw files", + "nl": "Verberg raw bestanden", + "de": "Rohdateien ausblenden" + }, + "MessageDisplayOptionsSwitchButtonIsSingleItemOff": { + "en": "Load everything", + "nl": "Alles inladen", + "de": "Alles laden" + }, + "MessageDisplayOptionsSwitchButtonIsSingleItemOn": { + "en": "Small loading", + "nl": "Klein inladen", + "de": "Kleine Ladung" + }, + "MessageDisplayOptionsSwitchButtonIsSocketOn": { + "en": "Realtime updates", + "nl": "Realtime updates", + "de": "Echtzeitupdates" + }, + "MessageDisplayOptionsSwitchButtonIsSocketOff": { + "en": "Refresh yourself", + "nl": "Ververs zelf", + "de": "Selbst aktualisieren" + }, + "MessageDownloadSelection": { + "en": "Download selection", + "nl": "Download selectie", + "de": "Auswahl herunterladen" + }, + "MessageOriginalFile": { + "en": "Original file", + "nl": "Origineel bestand", + "de": "Originaldatei" + }, + "MessageThumbnailFile": { + "en": "Thumbnail", + "nl": "Thumbnail", + "de": "Vorschaubild" + }, + "MessageModalDatetime": { + "en": "Edit date and time", + "nl": "Datum en tijd bewerken", + "de": "Datum und Uhrzeit bearbeiten" + }, + "MessageYear": { + "en": "Year", + "nl": "Jaar", + "de": "Jahr" + }, + "MessageMonth": { + "en": "Month", + "nl": "Maand", + "de": "Monat" + }, + "MessageDate": { + "en": "Day", + "nl": "Dag", + "de": "Tag" + }, + "MessageTime": { + "en": "Time", + "nl": "Tijd", + "de": "Zeit" + }, + "MessageErrorDatetime": { + "en": "The date and time were entered incorrectly", + "nl": "De datum en tijd zijn incorrect ingegeven", + "de": "Das Datum und die Uhrzeit wurden falsch eingegeben" + }, + "MessageDeleteIntroText": { + "en": "Are you sure you want to delete this file from all devices?", + "nl": "Weet je zeker dat je dit bestand wilt verwijderen van alle devices?", + "de": "Möchten Sie diese Datei wirklich von allen Geräten löschen?" + }, + "MessageConnectionRealtimeError": { + "en": "The connection is not quite right. We are trying to fix it", + "nl": "De verbinding is niet helemaal oké. We proberen het te herstellen", + "de": "Die Verbindung stimmt nicht ganz. Wir versuchen es zu beheben" + }, + "MessageApplicationFailed": { + "en": "The application has failed. Please reload it to try it again", + "nl": "De verbinding is niet helemaal oké. Probeer te herladen", + "de": "Die Verbindung stimmt nicht ganz. Wir versuchen es zu beheben" + }, + "MessageNumberOfResults": { + "en": "results", + "nl": "resultaten", + "de": "Ergebnisse" + }, + "MessageNoResult": { + "en": "No result", + "nl": "Geen resultaat", + "de": "Kein Ergebnis" + }, + "MessageTryOtherQuery": { + "en": "Try another search query", + "nl": "Probeer een andere zoekopdracht", + "de": "Versuchen Sie eine andere Suchanfrage" + }, + "MessagePageNumberToken": { + "en": "Page {pageNumber} of ", + "nl": "Pagina {pageNumber} van ", + "de": "Seite {pageNumber} von " + }, + "MessageEmptyTrash": { + "en": "There is nothing in the trash", + "nl": "Er staat niets in de prullenmand", + "de": "Es befindet sich nichts im Papierkorb" + }, + "MessageNotFound": { + "en": "Not Found", + "nl": "Oeps niet gevonden", + "de": "Nicht gefunden" + }, + "MessageGoToHome": { + "en": "Go to the homepage", + "nl": "Ga naar de homepagina", + "de": "Gehe zur Startseite" }, "temp1": { - "en": "", - "nl": "" + "en": "More", + "nl": "Meer", + "de": "Mehr" } } diff --git a/starsky/starsky/clientapp/src/pages/not-found-page.tsx b/starsky/starsky/clientapp/src/pages/not-found-page.tsx index cb1acab0f4..575e5b0b45 100644 --- a/starsky/starsky/clientapp/src/pages/not-found-page.tsx +++ b/starsky/starsky/clientapp/src/pages/not-found-page.tsx @@ -2,6 +2,7 @@ import { FunctionComponent } from "react"; import Link from "../components/atoms/link/link"; import MenuDefault from "../components/organisms/menu-default/menu-default"; import useGlobalSettings from "../hooks/use-global-settings"; +import localization from "../localization/localization.json"; import { Language } from "../shared/language"; import { UrlQuery } from "../shared/url-query"; @@ -10,8 +11,8 @@ export const NotFoundPage: FunctionComponent = () => { const settings = useGlobalSettings(); const language = new Language(settings.language); - const MessageNotFound = language.text("Oeps niet gevonden", "Not Found"); - const MessageGoToHome = language.text("Ga naar de homepagina", "Go to the homepage"); + const MessageNotFound = language.key(localization.MessageNotFound); + const MessageGoToHome = language.key(localization.MessageGoToHome); return (
diff --git a/starsky/starsky/clientapp/src/router-app/router-app.spec.tsx b/starsky/starsky/clientapp/src/router-app/router-app.spec.tsx index 93b7b12f33..6a88f8656d 100644 --- a/starsky/starsky/clientapp/src/router-app/router-app.spec.tsx +++ b/starsky/starsky/clientapp/src/router-app/router-app.spec.tsx @@ -8,7 +8,8 @@ import { ISearchList } from "../hooks/use-searchlist"; import * as NotFoundPage from "../pages/not-found-page"; import * as SearchPage from "../pages/search-page"; import * as TrashPage from "../pages/trash-page"; -import RouterApp, { Router, RoutesConfig } from "./router-app"; +import RouterApp, { Router } from "./router-app"; +import { RoutesConfig } from "./routes-config"; describe("Router", () => { it("default", () => { diff --git a/starsky/starsky/clientapp/src/router-app/router-app.tsx b/starsky/starsky/clientapp/src/router-app/router-app.tsx index 39ee0dd1a8..88bdc5725d 100644 --- a/starsky/starsky/clientapp/src/router-app/router-app.tsx +++ b/starsky/starsky/clientapp/src/router-app/router-app.tsx @@ -1,38 +1,12 @@ -import { RouteObject, RouterProvider, createBrowserRouter } from "react-router-dom"; -import { AccountRegisterPage } from "../pages/account-register-page"; -import { ContentPage } from "../pages/content-page"; -import ImportPage from "../pages/import-page"; -import { LoginPage } from "../pages/login-page"; -import { NotFoundPage } from "../pages/not-found-page"; -import { PreferencesPage } from "../pages/preferences-page"; -import { SearchPage } from "../pages/search-page"; -import { TrashPage } from "../pages/trash-page"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { GlobalShortcuts } from "../shared/global-shortcuts/global-shortcuts"; +import { RoutesConfig } from "./routes-config"; -export const RoutesConfig: RouteObject[] = [ - { - path: "/", - element: , - errorElement: - }, - { path: "starsky/", element: }, - { path: "search", element: }, - { path: "starsky/search", element: }, - { path: "trash", element: }, - { path: "starsky/trash", element: }, - { path: "import", element: }, - { path: "starsky/import", element: }, - { path: "login", element: }, - { path: "starsky/login", element: }, - { path: "account/login", element: }, - { path: "starsky/account/login", element: }, - { path: "account/register", element: }, - { path: "starsky/account/register", element: }, - { path: "preferences", element: }, - { path: "starsky/preferences", element: }, - { path: "*", element: } -]; export const Router = createBrowserRouter(RoutesConfig); -const RouterApp = () => ; +const RouterApp = () => { + GlobalShortcuts(); + return ; +}; export default RouterApp; diff --git a/starsky/starsky/clientapp/src/router-app/routes-config.tsx b/starsky/starsky/clientapp/src/router-app/routes-config.tsx new file mode 100644 index 0000000000..8bd0af781c --- /dev/null +++ b/starsky/starsky/clientapp/src/router-app/routes-config.tsx @@ -0,0 +1,33 @@ +import { RouteObject } from "react-router-dom"; +import { AccountRegisterPage } from "../pages/account-register-page"; +import { ContentPage } from "../pages/content-page"; +import ImportPage from "../pages/import-page"; +import { LoginPage } from "../pages/login-page"; +import { NotFoundPage } from "../pages/not-found-page"; +import { PreferencesPage } from "../pages/preferences-page"; +import { SearchPage } from "../pages/search-page"; +import { TrashPage } from "../pages/trash-page"; + +export const RoutesConfig: RouteObject[] = [ + { + path: "/", + element: , + errorElement: + }, + { path: "starsky/", element: }, + { path: "search", element: }, + { path: "starsky/search", element: }, + { path: "trash", element: }, + { path: "starsky/trash", element: }, + { path: "import", element: }, + { path: "starsky/import", element: }, + { path: "login", element: }, + { path: "starsky/login", element: }, + { path: "account/login", element: }, + { path: "starsky/account/login", element: }, + { path: "account/register", element: }, + { path: "starsky/account/register", element: }, + { path: "preferences", element: }, + { path: "starsky/preferences", element: }, + { path: "*", element: } +]; diff --git a/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.spec.tsx b/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.spec.tsx new file mode 100644 index 0000000000..d0791dea24 --- /dev/null +++ b/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.spec.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import * as useHotKeysParent from "../../hooks/use-keyboard/use-hotkeys"; +import * as useLocation from "../../hooks/use-location/use-location"; +import { UrlQuery } from "../url-query"; +import { GlobalShortcuts } from "./global-shortcuts"; + +describe("GlobalShortcuts", () => { + it("command + shift + k", () => { + const locationObject = { + location: window.location, + navigate: jest.fn() + }; + + jest.spyOn(React, "useEffect").mockImplementationOnce((cb) => { + cb(); + }); + + const event = new KeyboardEvent("keydown", { + bubbles: true, + cancelable: true, + key: "k", + metaKey: true, + shiftKey: true + }); + + jest.spyOn(useHotKeysParent, "default").mockImplementationOnce((_, callback) => { + if (callback) { + callback(event); + } + }); + + const useLocationSpy = jest + .spyOn(useLocation, "default") + .mockImplementationOnce(() => locationObject); + + GlobalShortcuts(); + + expect(useLocationSpy).toHaveBeenCalled(); + expect(locationObject.navigate).toHaveBeenCalled(); + expect(locationObject.navigate).toHaveBeenCalledWith(new UrlQuery().UrlPreferencesPage()); + }); +}); diff --git a/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.tsx b/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.tsx new file mode 100644 index 0000000000..b0dfc47fe2 --- /dev/null +++ b/starsky/starsky/clientapp/src/shared/global-shortcuts/global-shortcuts.tsx @@ -0,0 +1,17 @@ +import useHotKeys from "../../hooks/use-keyboard/use-hotkeys"; +import useLocation from "../../hooks/use-location/use-location"; +import { UrlQuery } from "../url-query"; + +export function GlobalShortcuts() { + const history = useLocation(); + + // used in desktop to route from menu + // command + shift + k + useHotKeys( + { key: "k", shiftKey: true, ctrlKeyOrMetaKey: true }, + () => { + history.navigate(new UrlQuery().UrlPreferencesPage()); + }, + [] + ); +} diff --git a/starsky/starsky/clientapp/src/shared/keyboard.spec.ts b/starsky/starsky/clientapp/src/shared/keyboard.spec.ts index 6716bf48fb..b253639b02 100644 --- a/starsky/starsky/clientapp/src/shared/keyboard.spec.ts +++ b/starsky/starsky/clientapp/src/shared/keyboard.spec.ts @@ -5,6 +5,14 @@ describe("keyboard", () => { describe("isInForm", () => { it("null input", () => { // new EventTarget() = not supported in Safari + + const result = keyboard.isInForm(undefined); + + expect(result).toBeNull(); + }); + + it("null input 2", () => { + // new EventTarget() = not supported in Safari const eventTarget: EventTarget = new EventTarget(); const event = new KeyboardEvent("keydown", { keyCode: 37, @@ -52,6 +60,47 @@ describe("keyboard", () => { }); describe("SetFocusOnEndField", () => { + it("no child items", () => { + const focusSpy = jest.fn(); + new Keyboard().SetFocusOnEndField({ + focus: focusSpy, + childNodes: [] + } as unknown as HTMLDivElement); + + expect(focusSpy).toHaveBeenCalledTimes(1); + }); + + it("text content", () => { + const focusSpy = jest.fn(); + new Keyboard().SetFocusOnEndField({ + focus: focusSpy, + childNodes: [{}] // text content is missing + } as unknown as HTMLDivElement); + + expect(focusSpy).toHaveBeenCalledTimes(0); + }); + + it("missing range", () => { + const focusSpy = jest.fn(); + const selectionSpy = jest.spyOn(window, "getSelection").mockImplementationOnce(() => null); + jest.spyOn(document, "createRange").mockImplementationOnce(() => { + return { + setStart: jest.fn(), + collapse: jest.fn() + } as unknown as Range; + }); + new Keyboard().SetFocusOnEndField({ + focus: focusSpy, + childNodes: [ + { + textContent: "hi" + } + ] + } as unknown as HTMLDivElement); + + expect(selectionSpy).toHaveBeenCalledTimes(1); + }); + it("input", () => { const target = document.createElement("div"); target.className = "test"; diff --git a/starsky/starsky/clientapp/src/shared/language.spec.ts b/starsky/starsky/clientapp/src/shared/language.spec.ts index 4e2870037a..baeafb7b7b 100644 --- a/starsky/starsky/clientapp/src/shared/language.spec.ts +++ b/starsky/starsky/clientapp/src/shared/language.spec.ts @@ -5,11 +5,11 @@ describe("keyboard", () => { describe("text", () => { it("get different content (dutch)", () => { - const result = language.text("dutch", "english"); + const result = language.text("dutch", "english", "deutsch"); expect(result).toBe("dutch"); }); it("get different content (english)", () => { - const result = new Language(SupportedLanguages.en).text("dutch", "english"); + const result = new Language(SupportedLanguages.en).text("dutch", "english", "deutsch"); expect(result).toBe("english"); }); }); @@ -18,17 +18,51 @@ describe("keyboard", () => { it("get different content (dutch)", () => { const result = language.key({ nl: "dutch", - en: "english" + en: "english", + de: "deutsch" }); expect(result).toBe("dutch"); }); it("get different content (english)", () => { const result = new Language(SupportedLanguages.en).key({ nl: "dutch", - en: "english" + en: "english", + de: "deutsch" }); expect(result).toBe("english"); }); + + it("replace keys - english", () => { + const data = { + nl: "Het onderstaande veld mag maximaal {maxlength} tekens hebben", + en: "The field below can have a maximum of {maxlength} characters", + de: "Das Feld unten kann maximal {maxlength} Zeichen enthalten" + }; + const maxlength = 14; + + const result = new Language(SupportedLanguages.en).key( + data, + ["{maxlength}"], + [maxlength.toString()] + ); + expect(result).toBe("The field below can have a maximum of 14 characters"); + }); + + it("replace keys - german", () => { + const data = { + nl: "Het onderstaande veld mag maximaal {maxlength} tekens hebben", + en: "The field below can have a maximum of {maxlength} characters", + de: "Das Feld unten kann maximal {maxlength} Zeichen enthalten" + }; + const maxlength = 14; + + const result = new Language(SupportedLanguages.de).key( + data, + ["{maxlength}"], + [maxlength.toString()] + ); + expect(result).toBe("Das Feld unten kann maximal 14 Zeichen enthalten"); + }); }); describe("token", () => { diff --git a/starsky/starsky/clientapp/src/shared/language.ts b/starsky/starsky/clientapp/src/shared/language.ts index ccbbb54b07..d1dc2f6d0e 100644 --- a/starsky/starsky/clientapp/src/shared/language.ts +++ b/starsky/starsky/clientapp/src/shared/language.ts @@ -1,6 +1,9 @@ +import { ILanguageLocalization } from "../interfaces/ILanguageLocalization"; + export enum SupportedLanguages { nl = "nl", - en = "en" + en = "en", + de = "de" } export class Language { @@ -11,25 +14,30 @@ export class Language { this.selectedLanguage = selectedLanguage; } - private selectedLanguage: SupportedLanguages; + private readonly selectedLanguage: SupportedLanguages; /** * WIP - * @param key * @returns + * @param content */ - public key(content: { en: string; nl: string }): string { - return this.text(content.nl, content.en); + public key(content: ILanguageLocalization, token?: string[], dynamicValue?: string[]): string { + const text = this.text(content.nl, content.en, content.de); + if (!token || !dynamicValue) { + return text; + } + return this.token(text, token, dynamicValue); } /** * Get the right content based on the language * Map used to be Map and nl = "nl" as any */ - public text(nl: string, en: string): string { + public text(nl: string, en: string, de: string): string { const selectedLanguageMap = new Map([ [SupportedLanguages.nl, nl], - [SupportedLanguages.en, en] + [SupportedLanguages.en, en], + [SupportedLanguages.de, de] ]); const content = selectedLanguageMap.get(this.selectedLanguage); diff --git a/starsky/starsky/clientapp/src/shared/url-query.ts b/starsky/starsky/clientapp/src/shared/url-query.ts index d227f7dfbc..8a8bf79e00 100644 --- a/starsky/starsky/clientapp/src/shared/url-query.ts +++ b/starsky/starsky/clientapp/src/shared/url-query.ts @@ -144,6 +144,10 @@ export class UrlQuery { return `${this.prefix}/api/account/permissions`; }; + public KeyAccountPermissionAppSettingsWrite = (): string => { + return "AppSettingsWrite"; + }; + /** * Keep colorClass in URL */ @@ -325,7 +329,15 @@ export class UrlQuery { }; public UrlApiFeaturesAppSettings = (): string => { - return this.prefix + "/api/env/features"; + return this.prefix + "/api/env/features?v=0.6.0-beta.2"; + }; + + public UrlApiDesktopEditorOpenAmountConfirmationChecker = (): string => { + return `${this.prefix}/api/desktop-editor/amount-confirmation`; + }; + + public UrlApiDesktopEditorOpen = (): string => { + return `${this.prefix}/api/desktop-editor/open`; }; /** diff --git a/starsky/starsky/clientapp/src/style/css/20-content.css b/starsky/starsky/clientapp/src/style/css/20-content.css index 06bf4910c1..16bb2ba57e 100644 --- a/starsky/starsky/clientapp/src/style/css/20-content.css +++ b/starsky/starsky/clientapp/src/style/css/20-content.css @@ -100,6 +100,11 @@ .content--text { padding: 10px; } + +.content--text.no-left-padding { + padding-left: 0px; +} + .content--text .date { display: inline-block; margin-right: 10px; diff --git a/starsky/starsky/clientapp/vite.config.ts b/starsky/starsky/clientapp/vite.config.ts index 6233d54087..6f5e3cebf5 100644 --- a/starsky/starsky/clientapp/vite.config.ts +++ b/starsky/starsky/clientapp/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ outDir: "build", assetsDir: "assets", assetsInlineLimit: 0, - chunkSizeWarningLimit: 600 + chunkSizeWarningLimit: 650 }, optimizeDeps: { include: ["leaflet", "core-js", "react", "react-dom", "react-router-dom"] diff --git a/starsky/starsky/readme.md b/starsky/starsky/readme.md index c4e42cbcd8..ef2cd3f11a 100644 --- a/starsky/starsky/readme.md +++ b/starsky/starsky/readme.md @@ -1,98 +1,135 @@ # Web API application + ## List of [Starsky](../../readme.md) Projects - * [By App documentation](../../starsky/readme.md) _database photo index & import index project_ + +* [By App documentation](../../starsky/readme.md) _database photo index & import index project_ * __[starsky](../../starsky/starsky/readme.md) web api application / interface__ - * [clientapp](../../starsky/starsky/clientapp/readme.md) _react front-end application_ - * [starskyImporterCli](../../starsky/starskyimportercli/readme.md) _import command line interface_ + * [clientapp](../../starsky/starsky/clientapp/readme.md) _react front-end application_ + * [starskyImporterCli](../../starsky/starskyimportercli/readme.md) _import command line + interface_ * [starskyGeoCli](../../starsky/starskygeocli/readme.md) _gpx sync and reverse 'geo tagging'_ - * [starskyWebHtmlCli](../../starsky/starskywebhtmlcli/readme.md) _publish web images to a content package_ - * [starskyWebFtpCli](../../starsky/starskywebftpcli/readme.md) _copy a content package to a ftp service_ + * [starskyWebHtmlCli](../../starsky/starskywebhtmlcli/readme.md) _publish web images to a + content package_ + * [starskyWebFtpCli](../../starsky/starskywebftpcli/readme.md) _copy a content package to a ftp + service_ * [starskyAdminCli](../../starsky/starskyadmincli/readme.md) _manage user accounts_ - * [starskySynchronizeCli](../../starsky/starskysynchronizecli/readme.md) _check if disk changes are updated in the database_ - * [starskyThumbnailCli](../../starsky/starskythumbnailcli/readme.md) _speed web performance by generating smaller images_ - * [Starsky Business Logic](../../starsky/starskybusinesslogic/readme.md) _business logic libraries (.NET)_ + * [starskySynchronizeCli](../../starsky/starskysynchronizecli/readme.md) _check if disk changes + are updated in the database_ + * [starskyThumbnailCli](../../starsky/starskythumbnailcli/readme.md) _speed web performance by + generating smaller images_ + * [Starsky Business Logic](../../starsky/starskybusinesslogic/readme.md) _business logic + libraries (.NET)_ * [starskyTest](../../starsky/starskytest/readme.md) _mstest unit tests (for .NET)_ - * [starsky-tools](../../starsky-tools/readme.md) _nodejs tools to add-on tasks_ - * [Starsky Desktop](../../starskydesktop/readme.md) _Desktop Application_ +* [starsky-tools](../../starsky-tools/readme.md) _nodejs tools to add-on tasks_ +* [Starsky Desktop](../../starskydesktop/readme.md) _Desktop Application_ * [Download Desktop App](https://docs.qdraw.nl/download/) _Windows and Mac OS version_ - * [Changelog](../../history.md) _Release notes and history_ +* [Changelog](../../history.md) _Release notes and history_ ## starsky/starsky docs ### Structure configuration: When setup Starksy there are two options to configure the installation. -There is a list of required settings. First the `appsettings.json` is loaded and the environment variables are overwriting features. +There is a list of required settings. First the `appsettings.json` is loaded and the environment +variables are overwriting features. The command line arguments are shortcuts to set an in-app environment variable. ### The order of reading settings -You could use machine specific configuration files: appsettings.machinename.json _(and replace machinename with your computer name in lowercase)_ -1. You can use `appsettings.json` inside the application folder to set base settings. - The order of this files is used to get the values from the appsettings - - `/bin/Debug/net8.0/appsettings.patch.json` - - `/bin/Debug/net8.0/appsettings.default.json` - - `/bin/Debug/net8.0/appsettings.computername.patch.json` - - `/bin/Debug/net8.0/appsettings.json` - - `/bin/Debug/net8.0/appsettings.computername.json` - -2. Use Environment variables to overwrite those base settings + +You could use machine specific configuration files: appsettings.machinename.json _(and replace +machinename with your computer name in lowercase)_ + +1. You can use `appsettings.json` inside the application folder to set base settings. + The order of this files is used to get the values from the appsettings + - `/bin/Debug/net8.0/appsettings.patch.json` + - `/bin/Debug/net8.0/appsettings.default.json` + - `/bin/Debug/net8.0/appsettings.computername.patch.json` + - `/bin/Debug/net8.0/appsettings.json` + - `/bin/Debug/net8.0/appsettings.computername.json` + +2. Use Environment variables to overwrite those base settings For `ThumbnailTempFolder` use `app__ThumbnailTempFolder` ([source](https://github.com/aspnet/Configuration/commit/cafd2e53eb71a6d0cecc60a9e38ea1df2dafb916)) - Dictionaries can be used this way: `app__accountRolesByEmailRegisterOverwrite__test@mail.be` -3. Command line arguments in the Cli applications to set in-app environment variables + Dictionaries can be used this way: `app__accountRolesByEmailRegisterOverwrite__test@mail.be` +3. Command line arguments in the Cli applications to set in-app environment variables ### Required settings to start -1. To start it is __not__ mandatory to adjust any settings. -### Recommend settings -1. `ThumbnailTempFolder` - For storing thumbnails (default: `./bin/Debug/net8.0/thumbnailTempFolder`) -2. `StorageFolder` - For the main photo directory (default: `./bin/Debug/net8.0/storageFolder`) -3. `DatabaseType` - `mysql`, `sqlite` or `inmemorydatabase` are supported (default: `sqlite`) -4. `DatabaseConnection` - The connection-string to the database (default: `./bin/Debug/net8.0/data.db`) -5. `CameraTimeZone` - The timezone of the Camera, for example `Europe/Amsterdam` (defaults to your local timezone) +1. To start it is __not__ mandatory to adjust any settings. However it is recommended to change the + `StorageFolder` to a folder on your local machine where the picture should be located. + +### Recommend settings {#recommend-settings} + +1. `ThumbnailTempFolder` - For storing thumbnails ( + default: `./bin/Debug/net8.0/thumbnailTempFolder`) +2. `StorageFolder` - For the main photo directory (default: `./bin/Debug/net8.0/storageFolder`) +3. `DatabaseType` - `mysql`, `sqlite` or `inmemorydatabase` are supported (default: `sqlite`) +4. `DatabaseConnection` - The connection-string to the database ( + default: `./bin/Debug/net8.0/data.db`) +5. `CameraTimeZone` - The timezone of the Camera, for example `Europe/Amsterdam` (defaults to your + local timezone) ### Optional settings + 1. `Structure` - The structure that will be used when you import files, _has a default fallback_. -2. `DependenciesFolder` - where store the data of external dependencies used _default folder in project_ +2. `DependenciesFolder` - where store the data of external dependencies used _default folder in + project_ 3. `ReadOnlyFolders` - Accepts a list of folders that never may be edited, _defaults a empty list_ 4. `AddMemoryCache` - Enable caching _(default true)_ - The only 2 build-in exceptions are when there are no accounts or you already logged in _(default false)_ + The only 2 build-in exceptions are when there are no accounts or you already logged in _(default + false)_ 5. `AddSwagger` - To show a user interface to show al REST-services _(default false)_ -6. `ExifToolImportXmpCreate` - is used to create at import time a xmp file based on the raw image _(default false)_ +6. `ExifToolImportXmpCreate` - is used to create at import time a xmp file based on the raw image _( + default false)_ 7. `AddSwaggerExport` - To Export Swagger definitions on startup _(default false)_ 8. `AddLegacyOverwrite`- Read Only value for ("Mono.Runtime") _(default false)_ 9. `Verbose` - show more console logging _(default false)_ 10. `WebFtp` - ftp path, this is used by starskyWebFtpCli -11. `PublishProfiles` - settings to configure publish output, used by starskyWebHtmlCli and publish button +11. `PublishProfiles` - settings to configure publish output, used by starskyWebHtmlCli and publish + button 12. `ExifToolPath` - A path to Exiftool.exe _to ignore the included ExifTool_ 13. `isAccountRegisterOpen` - Allow everyone to register an account _(default false)_ -14. `AccountRegisterDefaultRole` When a user is new and register an account, give it the role User or Administrator _(default User)_ -15. `useHttpsRedirection` - Redirect users to https page. You should enable before going to production. - This toggle is always disabled in debug/develop mode _(default false)_ -16. `httpsOn` Set all cookies in https Mode. You should enable before going to production. _(default false)_ +14. `AccountRegisterDefaultRole` When a user is new and register an account, give it the role User + or Administrator _(default User)_ +15. `useHttpsRedirection` - Redirect users to https page. You should enable before going to + production. + This toggle is always disabled in debug/develop mode _(default false)_ +16. `httpsOn` Set all cookies in https Mode. You should enable before going to production. _(default + false)_ 17. `Name` Name of the application, does not have much effect _(default Starsky)_ 18. `AppSettingsPath` To store the settings by user in the AppData folder _(default empty string)_ 19. `UseRealtime` Update the user interface realtime _default true_ 20. `UseDiskWatcher` Watch the disk for changes and update the database _default true_ 21. `CheckForUpdates` Check if there are updates on github and notify the user _default true_ -22. `SyncIgnore` Ignore pattern to not include disk items while running sync, uses always unix style and startsWith _default list with: /lost+found_ +22. `SyncIgnore` Ignore pattern to not include disk items while running sync, uses always unix style + and startsWith _default list with: /lost+found_ 23. `ImportIgnore` ImportIgnore filter _default list with: "lost+found" ".Trashes"_ 24. `MaxDegreesOfParallelism` Number of jobs running in background _default 6_ 25. `MetaThumbnailOnImport` Create small thumbnails after import, is very fast _default true_ 26. `EnablePackageTelemetry` Telemetry is send for service improvement _default true_ 27. `EnablePackageTelemetryDebug` Debug Telemetry _default false_ -28. `AddSwaggerExportExitAfter` Quit application after exporting swagger files, should have `AddSwagger` and `AddSwaggerExport` enabled _default false_ +28. `AddSwaggerExportExitAfter` Quit application after exporting swagger files, should + have `AddSwagger` and `AddSwaggerExport` enabled _default false_ 29. `NoAccountLocalhost` No login needed when on localhost, used in Desktop App 30. `VideoUseLocalTime` Use localtime by Camera make and model instead of UTC 31. `SyncOnStartup` Sync Database on changes since latest start _default true_ -32. `ThumbnailGenerationIntervalInMinutes` Interval to generate thumbnails, to disable use value lower than 3 _default 15_ -33. `GeoFilesSkipDownloadOnStartup` Skip download of GeoFiles on startup, _recommend to keep this false or null_ - _default false_ -34. `ExiftoolSkipDownloadOnStartup` Skip download of Exiftool on startup, _recommend to keep this false or null_ - _default false_ -35. `AccountRolesByEmailRegisterOverwrite` Overwrite the default role for a user by email address, _default empty list_ -36. `OpenTelemetry` See logging in an external service, _default no enabled_ see [OpenTelemetry](https://docs.qdraw.nl/docs/developer-guide/logging/opentelemetry.md) - +32. `ThumbnailGenerationIntervalInMinutes` Interval to generate thumbnails, to disable use value + lower than 3 _default 15_ +33. `GeoFilesSkipDownloadOnStartup` Skip download of GeoFiles on startup, _recommend to keep this + false or null_ - _default false_ +34. `ExiftoolSkipDownloadOnStartup` Skip download of Exiftool on startup, _recommend to keep this + false or null_ - _default false_ +35. `AccountRolesByEmailRegisterOverwrite` Overwrite the default role for a user by email address, + _default empty list_ +36. `OpenTelemetry` See logging in an external service, _default no enabled_ + see [OpenTelemetry](https://docs.qdraw.nl/docs/developer-guide/logging/opentelemetry.md) +37. `UseLocalDesktop` Enable local desktop features (hide trash in Ui / Open in Application) + _default false_ _in app true_ +38. `DefaultDesktopEditor` List of Properties that contain the default editor by imageFormat + _default none_ ### Appsettings.json example + ```json { "App": { @@ -111,18 +148,24 @@ You could use machine specific configuration files: appsettings.machinename.json > Note: When using a boolean in the json add quotes. Booleans without quotes are ignored -> Tip: When using the `mysql`-setting, make sure the database uses `utf8mb4` and as collate `utf8mb4_unicode_ci` to avoid encoding errors. +> Tip: When using the `mysql`-setting, make sure the database uses `utf8mb4` and as +> collate `utf8mb4_unicode_ci` to avoid encoding errors. #### Appsettings Notes -1. Structure uses slash as directory separators for Linux and Windows -2. The settings: `ExifToolPath`, `ThumbnailTempFolder` and `StorageFolder` uses the system path directory separators -3. When using Windows please double escape (`\\`) system path's + +1. Structure uses slash as directory separators for Linux and Windows +2. The settings: `ExifToolPath`, `ThumbnailTempFolder` and `StorageFolder` uses the system path + directory separators +3. When using Windows please double escape (`\\`) system path's ### Warmup script + The default behavior of .NET is to load everything first. -To be sure that the application is warm before someone arrives, please check `tools/starsky-warmup.sh`. +To be sure that the application is warm before someone arrives, please +check `tools/starsky-warmup.sh`. ### Search Docs + Advanced queries are supported by the basic search engine. __All text (not number or date) driven search queries use a contain search__ @@ -178,60 +221,78 @@ __All text (not number or date) driven search queries use a contain search__ | __software__ | -software:"photoshop" | Last edited this app | ### Rest API documentation -Starsky has a Json restful API. There is a Swagger documentation available at `/swagger/index.html` + +Starsky has a Json restful API. There is a Swagger documentation available at `/swagger/index.html` and in the documentation there is a API chapter > Tip: Breaking changes are documented in `./history.md` ### Swagger / OpenAPI + Swagger is an open-source software framework backed by a large ecosystem of tools that helps developers design, build, document, and consume RESTful Web services. There is an swagger definition. You could enable this by setting the following values: -By default this feature is disabled, please use the `AddSwagger` definition in the AppSettings or use the following environment variable: +By default this feature is disabled, please use the `AddSwagger` definition in the AppSettings or +use the following environment variable: ``` app__AddSwagger=true ``` This is the default location of the swagger documentation + ``` http://localhost:4000/swagger ``` ### Known 'There are critical errors in the following components:' -When the UI starts there is an Health API check to make sure that some important components works good + +When the UI starts there is an Health API check to make sure that some important components works +good #### Disk Space errors + - __Storage_StorageFolder__ There is not enough disk space available on the storage folder location -- __Storage_ThumbnailTempFolder__ There is not enough disk space available on the thumbnails folder location +- __Storage_ThumbnailTempFolder__ There is not enough disk space available on the thumbnails folder + location - __Storage_TempFolder__ There is not enough disk space available on the temp folder location #### Folder or file not exist errors + - __Exist_StorageFolder__ The Storage Folder does not exist, please create it first. - __Exist_TempFolder__ The Temp Folder does not exist, please create it first. - __Exist_ExifToolPath__ ExifTool is not linked, you need this to write meta data to files.ExifTool. - Try to remove the _temp folder_ and run the Application again. + Try to remove the _temp folder_ and run the Application again. - __Exist_ThumbnailTempFolder__ The Thumbnail cache Folder does not exist, please create it first. #### Date issues -- __DateAssemblyHealthCheck__ this setting checks if your current datetime is newer than when this application is build + +- __DateAssemblyHealthCheck__ this setting checks if your current datetime is newer than when this + application is build #### ApplicationDbContext, Mysql or Sqlite + There is also a check to make sure the database runs good #### Application Insights -Health issues are also reported to Microsoft Application Insights This only is when a valid key is configured. + +Health issues are also reported to Microsoft Application Insights This only is when a valid key is +configured. ### Known issues #### DiskWatcher in combination with child folders that have no access + When using `useDiskwatcher: true` and there are child folders that are not allowed to read For example the `lost+found` folder + ``` drwx------ 2 root root 16K Apr 16 2018 lost+found ``` + Then DiskWatcher is stopping and retry 20 times before the state will be disabled + ``` [DiskWatcher] (catch-ed) Access to the path '/mnt/external_disk/lost+found' is denied ``` @@ -241,7 +302,8 @@ Solution: make sure that all child folder are accessible #### DiskWatcher in combination with Mac OS APFS Disk When you set `/System/Volumes/Data` to watch for changes this makes the application crash with -`System.ArgumentOutOfRangeException` when a single file is changed. There is currently no solution for this problem other then don't use the Diskwatcher with this location. +`System.ArgumentOutOfRangeException` when a single file is changed. There is currently no solution +for this problem other then don't use the Diskwatcher with this location. ```c# Unhandled exception. System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. diff --git a/starsky/starsky/starsky.csproj b/starsky/starsky/starsky.csproj index 499347d482..71aed3f202 100644 --- a/starsky/starsky/starsky.csproj +++ b/starsky/starsky/starsky.csproj @@ -58,8 +58,8 @@ 0 - - + + @@ -143,6 +143,7 @@ + @@ -165,7 +166,7 @@ - + diff --git a/starsky/starskybusinesslogic/readme.md b/starsky/starskybusinesslogic/readme.md index fd6579bac4..d14763ac46 100644 --- a/starsky/starskybusinesslogic/readme.md +++ b/starsky/starskybusinesslogic/readme.md @@ -1,21 +1,29 @@ # Business Logic + ## List of [Starsky](../../readme.md) Projects - * [By App documentation](../../starsky/readme.md) _database photo index & import index project_ + +* [By App documentation](../../starsky/readme.md) _database photo index & import index project_ * [starsky](../../starsky/starsky/readme.md) _web api application / interface_ - * [clientapp](../../starsky/starsky/clientapp/readme.md) _react front-end application_ - * [starskyImporterCli](../../starsky/starskyimportercli/readme.md) _import command line interface_ + * [clientapp](../../starsky/starsky/clientapp/readme.md) _react front-end application_ + * [starskyImporterCli](../../starsky/starskyimportercli/readme.md) _import command line + interface_ * [starskyGeoCli](../../starsky/starskygeocli/readme.md) _gpx sync and reverse 'geo tagging'_ - * [starskyWebHtmlCli](../../starsky/starskywebhtmlcli/readme.md) _publish web images to a content package_ - * [starskyWebFtpCli](../../starsky/starskywebftpcli/readme.md) _copy a content package to a ftp service_ + * [starskyWebHtmlCli](../../starsky/starskywebhtmlcli/readme.md) _publish web images to a + content package_ + * [starskyWebFtpCli](../../starsky/starskywebftpcli/readme.md) _copy a content package to a ftp + service_ * [starskyAdminCli](../../starsky/starskyadmincli/readme.md) _manage user accounts_ - * [starskySynchronizeCli](../../starsky/starskysynchronizecli/readme.md) _check if disk changes are updated in the database_ - * [starskyThumbnailCli](../../starsky/starskythumbnailcli/readme.md) _speed web performance by generating smaller images_ - * __[Starsky Business Logic](../../starsky/starskybusinesslogic/readme.md) business logic libraries (.NET)__ + * [starskySynchronizeCli](../../starsky/starskysynchronizecli/readme.md) _check if disk changes + are updated in the database_ + * [starskyThumbnailCli](../../starsky/starskythumbnailcli/readme.md) _speed web performance by + generating smaller images_ + * __[Starsky Business Logic](../../starsky/starskybusinesslogic/readme.md) business logic + libraries (.NET)__ * [starskyTest](../../starsky/starskytest/readme.md) _mstest unit tests (for .NET)_ - * [starsky-tools](../../starsky-tools/readme.md) _nodejs tools to add-on tasks_ - * [Starsky Desktop](../../starskydesktop/readme.md) _Desktop Application_ +* [starsky-tools](../../starsky-tools/readme.md) _nodejs tools to add-on tasks_ +* [Starsky Desktop](../../starskydesktop/readme.md) _Desktop Application_ * [Download Desktop App](https://docs.qdraw.nl/download/) _Windows and Mac OS version_ - * [Changelog](../../history.md) _Release notes and history_ +* [Changelog](../../history.md) _Release notes and history_ ## Starsky Business Logic docs @@ -23,40 +31,39 @@ This is an overview of business logic ## Feature compare table -| Feature | Present | -|-------------------------------------------------------------------|---------| -| Anywhere secure access | ✅ | -| Native iOS and Android mobile apps | ❌ | -| Mac OS and Windows Desktop app | ✅ | -| Access controls with permissions and roles | ✴️ | -| User generation by Command Line | ✅ | -| Customized branded login page | ❌ | -| Out-of-the-box access from the web (when hosted) | ✅ | -| SaaS solution | ❌ | -| Multi tenant support | ❌ | -| Bulk metadata upload via CSV | ❌ | -| Bulk metadata edit via web interface | ✅ | -| Review, approve and publish uploads | ❌ | -| Batch or single file download | ✅ | -| Download permissions based on role | ❌ | -| Request access to file form | ❌ | -| Supports photos jpg, png, gif, tiff | ✅ | -| Supports video mp4 (H.264) | ✅ | -| Supports audio | ❌ | -| Supports IPTC, EXIF and XMP metadata | ✅ | -| All major browsers supported (Chrome, Safari, Mozilla) | ✅ | -| Internet Explorer support | ❌ | -| In-line editing in fields | ✅ | -| Localized platform English and Dutch | ✅ | -| Host the server version yourself using docker | ✅ | -| Host the server version yourself on a Windows/Mac/Linux | ✅ | - +| Feature | Present | +|---------------------------------------------------------|---------| +| Anywhere secure access | ✅ | +| Native iOS and Android mobile apps | ❌ | +| Mac OS and Windows Desktop app | ✅ | +| Access controls with permissions and roles | ✴️ | +| User generation by Command Line | ✅ | +| Customized branded login page | ❌ | +| Out-of-the-box access from the web (when hosted) | ✅ | +| SaaS solution | ❌ | +| Multi tenant support | ❌ | +| Bulk metadata upload via CSV | ❌ | +| Bulk metadata edit via web interface | ✅ | +| Review, approve and publish uploads | ❌ | +| Batch or single file download | ✅ | +| Download permissions based on role | ❌ | +| Request access to file form | ❌ | +| Supports photos jpg, png, gif, tiff | ✅ | +| Supports video mp4 (H.264) | ✅ | +| Supports audio | ❌ | +| Supports IPTC, EXIF and XMP metadata | ✅ | +| All major browsers supported (Chrome, Safari, Mozilla) | ✅ | +| Internet Explorer support | ❌ | +| In-line editing in fields | ✅ | +| Localized platform English and Dutch | ✅ | +| Host the server version yourself using docker | ✅ | +| Host the server version yourself on a Windows/Mac/Linux | ✅ | | Icon | Meaning of icon | |------|-----------------------| -| ✅ | fully implemented | -| ✴️ | is partly implemented | -| ❌ | not implemented | +| ✅ | fully implemented | +| ✴️ | is partly implemented | +| ❌ | not implemented | ## Project structure @@ -88,6 +95,18 @@ This is an overview of business logic | | Copy webhtmlpublish-ed items to an ftp server | └── starsky.feature.webhtmlpublish | Generate html content with photos. +| └── starsky.feature.realtime +| Real-time features +| └── starsky.feature.syncbackground +| Background synchronization features +| └── starsky.feature.search +| Search features +| └── starsky.feature.thumbnail +| Thumbnail-related features +| └── starsky.feature.settings +| Settings features +| └── starsky.feature.demo +| Demo features └── Foundation | | Modules in the Foundation layer are conceptually abstract and do not contain presentation in the form of renderings or views | └── starsky.foundation.accountmanagement @@ -112,10 +131,18 @@ This is an overview of business logic | | Generate Thumbnails | └── starsky.foundation.writemeta | Write though Exiftool +| └── starsky.foundation.worker +| Worker-related functionalities +| └── starsky.foundation.settings +| Settings-related functionalities +| └── starsky.foundation.native +| Native OS-related functionalities +| └── starsky.foundation.thumbnailmeta +| Thumbnail meta-related functionalities └── Project | This means the actual cohesive website or channel output from the implementation, such as the page types, layout and graphical design - └── starskycore - | To be depricated and replaced with feature and foundation services + └── starsky.project.web + | Services and helpers needed for the web application, but not for other applications └── starsky WebAPI presentation application (see ClientApp for more details about the UI) ``` diff --git a/starsky/starskycore/Attributes/ExcludeFromCoverageAttribute.cs b/starsky/starskycore/Attributes/ExcludeFromCoverageAttribute.cs deleted file mode 100644 index 260e1bcf43..0000000000 --- a/starsky/starskycore/Attributes/ExcludeFromCoverageAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System; - -namespace starskycore.Attributes -{ - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)] - public sealed class ExcludeFromCoverageAttribute : Attribute { } -} diff --git a/starsky/starskytest/AuthorizeAttributeTest/TestForNonAuthorizeAttributesTest.cs b/starsky/starskytest/AuthorizeAttributeTest/TestForNonAuthorizeAttributesTest.cs index c52f306263..f9eba86016 100644 --- a/starsky/starskytest/AuthorizeAttributeTest/TestForNonAuthorizeAttributesTest.cs +++ b/starsky/starskytest/AuthorizeAttributeTest/TestForNonAuthorizeAttributesTest.cs @@ -14,78 +14,96 @@ namespace starskytest.AuthorizeAttributeTest; [TestClass] public class TestForNonAuthorizeAttributesTest { - private static Type[] GetControllersInNamespace(Assembly assembly, string controllerNamespace) - { - return assembly.GetTypes().Where(types => string.Equals(types.Namespace, controllerNamespace, StringComparison.Ordinal)).ToArray(); - } + private static Type[] GetControllersInNamespace(Assembly assembly, string controllerNamespace) + { + return assembly.GetTypes().Where(types => + string.Equals(types.Namespace, controllerNamespace, StringComparison.Ordinal)) + .ToArray(); + } - private static (Assembly, string) GetAssembly() - { - var getEverythingBeforeLastDotRegex = new Regex(".*(?=\\.)", RegexOptions.None, TimeSpan.FromMilliseconds(100)); - var fullName = typeof(T).FullName; - if (string.IsNullOrEmpty(fullName)) - { - throw new Exception("Type does not have a FullName"); - } - var name = getEverythingBeforeLastDotRegex.Match(fullName).Value; - var assembly = typeof(T).Assembly; - return (assembly, name); - } + private static (Assembly, string) GetAssembly() + { + var getEverythingBeforeLastDotRegex = new Regex(".*(?=\\.)", RegexOptions.None, + TimeSpan.FromMilliseconds(200)); - [SuppressMessage("Usage", "S6602:\"Find\" method should be used instead of the \"FirstOrDefault\" extension")] - private static List GetControllerMethods(Type projectController) - { - var projectMethods = projectController.GetMethods(BindingFlags.Public | BindingFlags.Instance); + var fullName = typeof(T).FullName; + if ( string.IsNullOrEmpty(fullName) ) + { + throw new Exception("Type does not have a FullName"); + } - var (controllerBaseAssembly, controllerBaseName) = GetAssembly(); - var controllersBaseInNamespace = GetControllersInNamespace(controllerBaseAssembly, controllerBaseName); - var controllersBaseInNamespaceMethods = controllersBaseInNamespace.SelectMany(p => p.GetMethods()).ToArray(); - - var (controllerGenericTypeAssembly, controllerGenericTypeName) = GetAssembly(); - var controllersGenericTypeInNamespace = GetControllersInNamespace(controllerGenericTypeAssembly, controllerGenericTypeName); - var controllersGenericTypeNamespaceMethods = controllersGenericTypeInNamespace.SelectMany(p => p.GetMethods()).ToArray(); - - var allGenericControllerTypes = controllersBaseInNamespaceMethods.Concat(controllersGenericTypeNamespaceMethods).ToArray(); + var name = getEverythingBeforeLastDotRegex.Match(fullName).Value; + var assembly = typeof(T).Assembly; + return ( assembly, name ); + } - var projectsThatNotInheritFromControllerBase = new List(); - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var projectMethod in projectMethods) - { - var match = allGenericControllerTypes.FirstOrDefault(p => p.Name == projectMethod.Name); - if (match == null) - { - projectsThatNotInheritFromControllerBase.Add(projectMethod); - } - - } - - return projectsThatNotInheritFromControllerBase; - } + [SuppressMessage("Usage", + "S6602:\"Find\" method should be used instead of the \"FirstOrDefault\" extension")] + private static List GetControllerMethods(Type projectController) + { + var projectMethods = + projectController.GetMethods(BindingFlags.Public | BindingFlags.Instance); - [TestMethod] - public void TestForNonAuthorizeAttributes() - { - var (assembly, name) = GetAssembly(); - var controllersInNamespace = GetControllersInNamespace(assembly, name); + var (controllerBaseAssembly, controllerBaseName) = GetAssembly(); + var controllersBaseInNamespace = + GetControllersInNamespace(controllerBaseAssembly, controllerBaseName); + var controllersBaseInNamespaceMethods = + controllersBaseInNamespace.SelectMany(p => p.GetMethods()).ToArray(); - foreach (var controller in controllersInNamespace) - { - var methods = GetControllerMethods(controller); - foreach (var method in methods) - { - var authorizeAttributes = method.GetCustomAttributes(typeof(AuthorizeAttribute), true); - var authorizeParentAttributes = method.DeclaringType?.GetCustomAttributes(typeof(AuthorizeAttribute), true) ?? Array.Empty(); - var allowAnonymousAttributes = method.GetCustomAttributes(typeof(AllowAnonymousAttribute), true); - var allowAnonymousParentAttributes = method.DeclaringType?.GetCustomAttributes(typeof(AllowAnonymousAttribute), true) ?? Array.Empty(); + var (controllerGenericTypeAssembly, controllerGenericTypeName) = GetAssembly(); + var controllersGenericTypeInNamespace = + GetControllersInNamespace(controllerGenericTypeAssembly, controllerGenericTypeName); + var controllersGenericTypeNamespaceMethods = controllersGenericTypeInNamespace + .SelectMany(p => p.GetMethods()).ToArray(); - var attributes = new List(); - attributes.AddRange(authorizeAttributes); - attributes.AddRange(authorizeParentAttributes); - attributes.AddRange(allowAnonymousAttributes); - attributes.AddRange(allowAnonymousParentAttributes); + var allGenericControllerTypes = controllersBaseInNamespaceMethods + .Concat(controllersGenericTypeNamespaceMethods).ToArray(); - Assert.IsTrue(attributes.Count != 0, $"No AuthorizeAttribute found on {controller.FullName} {method.Name} method"); - } - } - } + var projectsThatNotInheritFromControllerBase = new List(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach ( var projectMethod in projectMethods ) + { + var match = allGenericControllerTypes.FirstOrDefault(p => p.Name == projectMethod.Name); + if ( match == null ) + { + projectsThatNotInheritFromControllerBase.Add(projectMethod); + } + } + + return projectsThatNotInheritFromControllerBase; + } + + [TestMethod] + public void TestForNonAuthorizeAttributes() + { + var (assembly, name) = GetAssembly(); + var controllersInNamespace = GetControllersInNamespace(assembly, name); + + foreach ( var controller in controllersInNamespace ) + { + var methods = GetControllerMethods(controller); + foreach ( var method in methods ) + { + var authorizeAttributes = + method.GetCustomAttributes(typeof(AuthorizeAttribute), true); + var authorizeParentAttributes = + method.DeclaringType?.GetCustomAttributes(typeof(AuthorizeAttribute), true) ?? + Array.Empty(); + var allowAnonymousAttributes = + method.GetCustomAttributes(typeof(AllowAnonymousAttribute), true); + var allowAnonymousParentAttributes = + method.DeclaringType?.GetCustomAttributes(typeof(AllowAnonymousAttribute), + true) ?? Array.Empty(); + + var attributes = new List(); + attributes.AddRange(authorizeAttributes); + attributes.AddRange(authorizeParentAttributes); + attributes.AddRange(allowAnonymousAttributes); + attributes.AddRange(allowAnonymousParentAttributes); + + Assert.IsTrue(attributes.Count != 0, + $"No AuthorizeAttribute found on {controller.FullName} {method.Name} method"); + } + } + } } diff --git a/starsky/starskytest/Controllers/AppSettingsControllerTest.cs b/starsky/starskytest/Controllers/AppSettingsControllerTest.cs index 177d824484..d16a7bc7a4 100644 --- a/starsky/starskytest/Controllers/AppSettingsControllerTest.cs +++ b/starsky/starskytest/Controllers/AppSettingsControllerTest.cs @@ -18,16 +18,16 @@ namespace starskytest.Controllers [TestClass] public sealed class AppSettingsControllerTest { - [TestMethod] public void ENV_StarskyTestEnv() { - var controller = new AppSettingsController(new AppSettings(),new FakeIUpdateAppSettingsByPath()); + var controller = + new AppSettingsController(new AppSettings(), new FakeIUpdateAppSettingsByPath()); var actionResult = controller.Env() as JsonResult; var resultAppSettings = actionResult?.Value as AppSettings; Assert.AreEqual("Starsky", resultAppSettings?.Name); } - + [TestMethod] public void ENV_StarskyTestEnv_ForceHtml() { @@ -38,40 +38,45 @@ public void ENV_StarskyTestEnv_ForceHtml() var actionResult = controller.Env() as JsonResult; var resultAppSettings = actionResult?.Value as AppSettings; Assert.AreEqual("Starsky", resultAppSettings?.Name); - Assert.AreEqual("text/html; charset=utf-8", + Assert.AreEqual("text/html; charset=utf-8", controller.ControllerContext.HttpContext.Response.Headers.ContentType.ToString()); } - + [TestMethod] public async Task UpdateAppSettings_Verbose() { var appSettings = new AppSettings(); var storage = new FakeIStorage(new List { "/" }); - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings,new FakeSelectorStorage(storage))); - - var actionResult = await controller.UpdateAppSettings(new AppSettingsTransferObject {Verbose = true}) as JsonResult; + var controller = new AppSettingsController(appSettings, + new UpdateAppSettingsByPath(appSettings, new FakeSelectorStorage(storage))); + + var actionResult = + await controller.UpdateAppSettings(new AppSettingsTransferObject { Verbose = true }) + as JsonResult; var result = actionResult?.Value as AppSettings; Assert.IsTrue(result?.Verbose); } - + [TestMethod] public async Task UpdateAppSettings_StorageFolder() { var appSettings = new AppSettings(); - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings, - new FakeSelectorStorage(new FakeIStorage(new List{ $"{Path.DirectorySeparatorChar}test"})))); - + var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath( + appSettings, + new FakeSelectorStorage( + new FakeIStorage(new List { $"{Path.DirectorySeparatorChar}test" })))); + controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = await controller.UpdateAppSettings(new AppSettingsTransferObject { - Verbose = true, - StorageFolder = $"{Path.DirectorySeparatorChar}test" + Verbose = true, StorageFolder = $"{Path.DirectorySeparatorChar}test" }) as JsonResult; var result = actionResult?.Value as AppSettings; Assert.IsTrue(result?.Verbose); - Assert.AreEqual(Path.DirectorySeparatorChar + PathHelper.AddBackslash("test"),result?.StorageFolder); + Assert.AreEqual(Path.DirectorySeparatorChar + PathHelper.AddBackslash("test"), + result?.StorageFolder); } [TestMethod] @@ -81,99 +86,99 @@ public async Task UpdateAppSettingsTest_IgnoreWhenEnvIsSet() "any_value"); var appSettings = new AppSettings(); - var controller = new AppSettingsController(appSettings, new FakeIUpdateAppSettingsByPath(new UpdateAppSettingsStatusModel - { - StatusCode = 403 - })); + var controller = new AppSettingsController(appSettings, + new FakeIUpdateAppSettingsByPath( + new UpdateAppSettingsStatusModel { StatusCode = 403 })); controller.ControllerContext.HttpContext = new DefaultHttpContext(); await controller.UpdateAppSettings( - new AppSettingsTransferObject - { - StorageFolder = "test" - }); + new AppSettingsTransferObject { StorageFolder = "test" }); Assert.AreEqual(403, controller.Response.StatusCode); } - + [TestMethod] public async Task UpdateAppSettingsTest_DirNotFound() { var appSettings = new AppSettings(); - var controller = new AppSettingsController(appSettings, new FakeIUpdateAppSettingsByPath(new UpdateAppSettingsStatusModel - { - StatusCode = 404 - })); + var controller = new AppSettingsController(appSettings, + new FakeIUpdateAppSettingsByPath( + new UpdateAppSettingsStatusModel { StatusCode = 404 })); controller.ControllerContext.HttpContext = new DefaultHttpContext(); - + await controller.UpdateAppSettings( - new AppSettingsTransferObject - { - StorageFolder = "not_found" - }); + new AppSettingsTransferObject { StorageFolder = "not_found" }); Assert.AreEqual(404, controller.Response.StatusCode); } - + [TestMethod] public async Task UpdateAppSettingsTest_StorageFolder_JsonCheck() { var storage = new FakeIStorage(new List { "test" }); Environment.SetEnvironmentVariable("app__storageFolder", string.Empty); - + var appSettings = new AppSettings { - AppSettingsPath = $"{Path.DirectorySeparatorChar}temp{Path.DirectorySeparatorChar}appsettings.json" + AppSettingsPath = + $"{Path.DirectorySeparatorChar}temp{Path.DirectorySeparatorChar}appsettings.json" }; - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings,new FakeSelectorStorage(storage))); + var controller = new AppSettingsController(appSettings, + new UpdateAppSettingsByPath(appSettings, new FakeSelectorStorage(storage))); await controller.UpdateAppSettings( - new AppSettingsTransferObject - { - Verbose = true, StorageFolder = "test" - }); + new AppSettingsTransferObject { Verbose = true, StorageFolder = "test" }); Assert.IsTrue(storage.ExistFile(appSettings.AppSettingsPath)); - var jsonContent= await StreamToStringHelper.StreamToStringAsync( + var jsonContent = await StreamToStringHelper.StreamToStringAsync( storage.ReadStream(appSettings.AppSettingsPath)); Assert.IsTrue(jsonContent.Contains("app\": {")); Assert.IsTrue(jsonContent.Contains("\"StorageFolder\": \"")); } - + [TestMethod] - public async Task UpdateAppSettings_UseLocalDesktopUi() + public async Task UpdateAppSettings_UseLocalDesktop() { var appSettings = new AppSettings(); var storage = new FakeIStorage(new List { "/" }); - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings,new FakeSelectorStorage(storage))); + var controller = new AppSettingsController(appSettings, + new UpdateAppSettingsByPath(appSettings, new FakeSelectorStorage(storage))); - var actionResult = await controller.UpdateAppSettings(new AppSettingsTransferObject {UseLocalDesktopUi = true}) as JsonResult; + var actionResult = + await controller.UpdateAppSettings( + new AppSettingsTransferObject { UseLocalDesktop = true }) as JsonResult; var result = actionResult?.Value as AppSettings; - Assert.IsTrue(result?.UseLocalDesktopUi); + Assert.IsTrue(result?.UseLocalDesktop); } - + [TestMethod] public async Task UpdateAppSettings_UseSystemTrash() { var appSettings = new AppSettings(); var storage = new FakeIStorage(new List { "/" }); - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings,new FakeSelectorStorage(storage))); - - var actionResult = await controller.UpdateAppSettings(new AppSettingsTransferObject {UseSystemTrash = true}) as JsonResult; + var controller = new AppSettingsController(appSettings, + new UpdateAppSettingsByPath(appSettings, new FakeSelectorStorage(storage))); + + var actionResult = + await controller.UpdateAppSettings( + new AppSettingsTransferObject { UseSystemTrash = true }) as JsonResult; var result = actionResult?.Value as AppSettings; Assert.IsTrue(result?.UseSystemTrash); } - + [TestMethod] public async Task UpdateAppSettings_Verbose_IgnoreSystemTrashValue() { var appSettings = new AppSettings(); var storage = new FakeIStorage(new List { "/" }); - var controller = new AppSettingsController(appSettings, new UpdateAppSettingsByPath(appSettings,new FakeSelectorStorage(storage))); - - var actionResult = await controller.UpdateAppSettings(new AppSettingsTransferObject {Verbose = true}) as JsonResult; + var controller = new AppSettingsController(appSettings, + new UpdateAppSettingsByPath(appSettings, new FakeSelectorStorage(storage))); + + var actionResult = + await controller.UpdateAppSettings(new AppSettingsTransferObject { Verbose = true }) + as JsonResult; var result = actionResult?.Value as AppSettings; - + Assert.AreEqual(appSettings.UseSystemTrash, result?.UseSystemTrash); } } diff --git a/starsky/starskytest/Controllers/AppSettingsFeaturesControllerTest.cs b/starsky/starskytest/Controllers/AppSettingsFeaturesControllerTest.cs index 89f2acc6de..bf286b0d51 100644 --- a/starsky/starskytest/Controllers/AppSettingsFeaturesControllerTest.cs +++ b/starsky/starskytest/Controllers/AppSettingsFeaturesControllerTest.cs @@ -4,7 +4,7 @@ using starsky.Controllers; using starsky.foundation.database.Models; using starsky.foundation.platform.Models; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; using starskytest.FakeMocks; namespace starskytest.Controllers; @@ -18,56 +18,54 @@ public void FeaturesViewTest() // Arrange var fakeIMoveToTrashService = new FakeIMoveToTrashService(new List()); var appSettingsFeaturesController = new AppSettingsFeaturesController( - fakeIMoveToTrashService, new AppSettings()); - + fakeIMoveToTrashService, new FakeIOpenEditorDesktopService(), new AppSettings()); + // Act var result = appSettingsFeaturesController.FeaturesView() as JsonResult; var json = result?.Value as EnvFeaturesViewModel; Assert.IsNotNull(json); - + // Assert Assert.IsNotNull(result); } - + [TestMethod] public void FeaturesViewTest_Disabled() { // Arrange var fakeIMoveToTrashService = new FakeIMoveToTrashService(new List(), false); var appSettingsFeaturesController = new AppSettingsFeaturesController( - fakeIMoveToTrashService, new AppSettings - { - UseLocalDesktopUi = false - }); - + fakeIMoveToTrashService, new FakeIOpenEditorDesktopService(false), + new AppSettings { UseLocalDesktop = false }); + // Act var result = appSettingsFeaturesController.FeaturesView() as JsonResult; var json = result?.Value as EnvFeaturesViewModel; Assert.IsNotNull(json); // Assert - Assert.IsFalse(json.UseLocalDesktopUi); + Assert.IsFalse(json.UseLocalDesktop); Assert.IsFalse(json.SystemTrashEnabled); + Assert.IsFalse(json.OpenEditorEnabled); } - + [TestMethod] public void FeaturesViewTest_Enabled() { // Arrange var fakeIMoveToTrashService = new FakeIMoveToTrashService(new List()); var appSettingsFeaturesController = new AppSettingsFeaturesController( - fakeIMoveToTrashService, new AppSettings - { - UseLocalDesktopUi = true - }); - + fakeIMoveToTrashService, new FakeIOpenEditorDesktopService(), + new AppSettings { UseLocalDesktop = true }); + // Act var result = appSettingsFeaturesController.FeaturesView() as JsonResult; var json = result?.Value as EnvFeaturesViewModel; Assert.IsNotNull(json); // Assert - Assert.IsTrue(json.UseLocalDesktopUi); + Assert.IsTrue(json.UseLocalDesktop); Assert.IsTrue(json.SystemTrashEnabled); + Assert.IsTrue(json.OpenEditorEnabled); } } diff --git a/starsky/starskytest/Controllers/DesktopEditorControllerTest.cs b/starsky/starskytest/Controllers/DesktopEditorControllerTest.cs new file mode 100644 index 0000000000..ef09835d47 --- /dev/null +++ b/starsky/starskytest/Controllers/DesktopEditorControllerTest.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.Controllers; +using starsky.feature.desktop.Models; +using starsky.feature.desktop.Service; +using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeMocks; + +namespace starskytest.Controllers; + +[TestClass] +public class DesktopEditorControllerTest +{ + [TestMethod] + public void OpenAmountConfirmationChecker_FeatureToggleEnabled() + { + var controller = new DesktopEditorController( + new OpenEditorDesktopService(new AppSettings(), + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List()))); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + + var result = controller.OpenAmountConfirmationChecker("/test.jpg;/test2.jpg"); + + var castedResult = ( JsonResult )result; + var boolValue = ( bool? )castedResult.Value; + // mock is always true + + Assert.IsTrue(boolValue); + } + + [TestMethod] + public async Task OpenAsync_FeatureToggleDisabled() + { + var controller = new DesktopEditorController(new OpenEditorDesktopService(new AppSettings(), + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List()))); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + + var result = await controller.OpenAsync("/test.jpg;/test2.jpg"); + var castedResult = ( BadRequestObjectResult )result; + + Assert.AreEqual(400, castedResult.StatusCode); + } + + [TestMethod] + public async Task OpenAsync_NoResultsBack() + { + var controller = new DesktopEditorController(new OpenEditorDesktopService( + new AppSettings { UseLocalDesktop = true }, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List()))); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + + var result = await controller.OpenAsync("/test.jpg;/test2.jpg"); + Assert.AreEqual(206, controller.HttpContext.Response.StatusCode); + + var castedResult = ( JsonResult )result; + var arrayValues = ( List? )castedResult.Value; + + Assert.AreEqual(0, arrayValues?.Count); + } + + [TestMethod] + public async Task OpenAsync_HappyFlow() + { + var preflight = new FakeIOpenEditorPreflight(new List + { + new PathImageFormatExistsAppPathModel + { + AppPath = "test", + Status = FileIndexItem.ExifStatus.Ok, + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg, + SubPath = "/test.jpg", + FullFilePath = "/test.jpg" + } + }); + var controller = new DesktopEditorController(new OpenEditorDesktopService( + new AppSettings { UseLocalDesktop = true }, + new FakeIOpenApplicationNativeService(new List(), "test"), + preflight)); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; + + var result = await controller.OpenAsync("/test.jpg;/test2.jpg"); + Assert.AreEqual(200, controller.HttpContext.Response.StatusCode); + + var castedResult = ( JsonResult )result; + var arrayValues = ( List? )castedResult.Value; + + Assert.AreEqual(1, arrayValues?.Count); + } +} diff --git a/starsky/starskytest/Controllers/DiskControllerTest.cs b/starsky/starskytest/Controllers/DiskControllerTest.cs index 712f7b4b37..72e2471710 100644 --- a/starsky/starskytest/Controllers/DiskControllerTest.cs +++ b/starsky/starskytest/Controllers/DiskControllerTest.cs @@ -22,10 +22,9 @@ using starsky.foundation.worker.Interfaces; using starsky.foundation.worker.Services; using starsky.foundation.writemeta.Interfaces; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { @@ -62,9 +61,9 @@ public DiskControllerTest() _createAnImage = new CreateAnImage(); var dict = new Dictionary { - {"App:StorageFolder", _createAnImage.BasePath}, - {"App:ThumbnailTempFolder", _createAnImage.BasePath}, - {"App:Verbose", "true"} + { "App:StorageFolder", _createAnImage.BasePath }, + { "App:ThumbnailTempFolder", _createAnImage.BasePath }, + { "App:Verbose", "true" } }; // Start using dependency injection var builder = new ConfigurationBuilder(); @@ -83,24 +82,24 @@ public DiskControllerTest() var serviceProvider = services.BuildServiceProvider(); // get the service var appSettings = serviceProvider.GetRequiredService(); - + var scopeFactory = serviceProvider.GetRequiredService(); - _query = new Query(context, appSettings, scopeFactory, new FakeIWebLogger(), memoryCache); + _query = new Query(context, appSettings, scopeFactory, new FakeIWebLogger(), + memoryCache); } private async Task InsertSearchData() { - _iStorage = new FakeIStorage(new List { "/" }, + _iStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); - var fileHashCode = (await new FileHash(_iStorage).GetHashCodeAsync(_createAnImage.DbPath)).Key; - + var fileHashCode = + ( await new FileHash(_iStorage).GetHashCodeAsync(_createAnImage.DbPath) ).Key; + if ( string.IsNullOrEmpty(await _query.GetSubPathByHashAsync(fileHashCode)) ) { await _query.AddItemAsync(new FileIndexItem { - FileName = "/", - ParentDirectory = "/", - IsDirectory = true + FileName = "/", ParentDirectory = "/", IsDirectory = true }); await _query.AddItemAsync(new FileIndexItem @@ -115,103 +114,94 @@ await _query.AddItemAsync(new FileIndexItem _query.GetObjectByFilePath(_createAnImage.DbPath); } - + [TestMethod] public async Task SyncControllerTest_Rename_NotFoundInIndex() { - - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; var fakeStorage = new FakeIStorage(); var storageSelector = new FakeSelectorStorage(fakeStorage); - - var controller = new DiskController(_query, storageSelector, + + var controller = new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()); controller.ControllerContext = context; - var result = await controller.Rename("/notfound-image.jpg", "/test.jpg") as NotFoundObjectResult; - - Assert.AreEqual(404,result?.StatusCode); + var result = + await controller.Rename("/notfound-image.jpg", "/test.jpg") as NotFoundObjectResult; + + Assert.AreEqual(404, result?.StatusCode); } - + [TestMethod] public async Task SyncControllerTest_BadRequest() { - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; var fakeStorage = new FakeIStorage(); var storageSelector = new FakeSelectorStorage(fakeStorage); - - var controller = new DiskController(_query, storageSelector, + + var controller = new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()); controller.ControllerContext = context; - var result = await controller.Rename(string.Empty, "/test.jpg") as BadRequestObjectResult; - - Assert.AreEqual(400,result?.StatusCode); + var result = + await controller.Rename(string.Empty, "/test.jpg") as BadRequestObjectResult; + + Assert.AreEqual(400, result?.StatusCode); } - + [TestMethod] public async Task SyncControllerTest_Rename_Good() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - - var fakeStorage = new FakeIStorage(new List { "/" }, + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var fakeStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); - + var controller = - new DiskController( _query, storageSelector, + new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()) { ControllerContext = context }; - + var result = await controller.Rename(_createAnImage.DbPath, "/test.jpg") as JsonResult; var list = result?.Value as List; - Assert.AreEqual(FileIndexItem.ExifStatus.Ok,list?.FirstOrDefault()?.Status); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, list?.FirstOrDefault()?.Status); - await _query.RemoveItemAsync((await _query.GetObjectByFilePathAsync("/test.jpg"))!); + await _query.RemoveItemAsync(( await _query.GetObjectByFilePathAsync("/test.jpg") )!); } - + [TestMethod] public async Task SyncControllerTest_Rename_WithCurrentStatusDisabled() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - - var fakeStorage = new FakeIStorage(new List { "/" }, + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var fakeStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); - + var controller = - new DiskController(_query, storageSelector, + new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()) { ControllerContext = context }; - - var result = await controller.Rename(_createAnImage.DbPath, "/test.jpg", true, false) as JsonResult; + + var result = + await controller.Rename(_createAnImage.DbPath, "/test.jpg", true, false) as + JsonResult; var list = result?.Value as List; - Assert.AreEqual(FileIndexItem.ExifStatus.Ok,list?[0].Status); - Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing,list?[1].Status); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, list?[0].Status); + Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, list?[1].Status); - await _query.RemoveItemAsync((await _query.GetObjectByFilePathAsync("/test.jpg"))!); + await _query.RemoveItemAsync(( await _query.GetObjectByFilePathAsync("/test.jpg") )!); } [TestMethod] @@ -219,134 +209,114 @@ public async Task SyncControllerTest_Rename_Good_SocketUpdate() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; var socket = new FakeIWebSocketConnectionsService(); - var fakeStorage = new FakeIStorage(new List { "/" }, + var fakeStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); - + var controller = - new DiskController(_query, storageSelector, - socket, new FakeINotificationQuery()) - { - ControllerContext = context - }; - + new DiskController(_query, storageSelector, + socket, new FakeINotificationQuery()) { ControllerContext = context }; + await controller.Rename(_createAnImage.DbPath, "/test.jpg"); - - Assert.AreEqual(1,socket.FakeSendToAllAsync.Count(p => !p.Contains("[system]"))); + + Assert.AreEqual(1, socket.FakeSendToAllAsync.Count(p => !p.Contains("[system]"))); Assert.IsTrue(socket.FakeSendToAllAsync[0].Contains("/test.jpg")); - - await _query.RemoveItemAsync((await _query.GetObjectByFilePathAsync("/test.jpg"))!); + + await _query.RemoveItemAsync(( await _query.GetObjectByFilePathAsync("/test.jpg") )!); } [TestMethod] public async Task SyncControllerTest_Mkdir_Good() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - - var fakeStorage = new FakeIStorage(new List { "/" }, + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var fakeStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); var controller = - new DiskController( _query, storageSelector, + new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()) { ControllerContext = context }; - + var result = await controller.Mkdir("/test_dir") as JsonResult; var list = result?.Value as List; - Assert.AreEqual(FileIndexItem.ExifStatus.Ok,list?.FirstOrDefault()?.Status); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, list?.FirstOrDefault()?.Status); } - + [TestMethod] public async Task SyncControllerTest_Mkdir_Good_SocketUpdate() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; var socket = new FakeIWebSocketConnectionsService(); - var fakeStorage = new FakeIStorage(new List { "/" }, + var fakeStorage = new FakeIStorage(new List { "/" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); var controller = - new DiskController(_query, storageSelector, - socket, new FakeINotificationQuery()) - { - ControllerContext = context - }; - + new DiskController(_query, storageSelector, + socket, new FakeINotificationQuery()) { ControllerContext = context }; + await controller.Mkdir("/test_dir"); - + var value = socket.FakeSendToAllAsync.Find(p => !p.StartsWith("[system]")); - + Assert.IsNotNull(value); Assert.IsTrue(value.Contains("/test_dir")); } - + [TestMethod] public async Task SyncControllerTest_Mkdir_Exist() { await InsertSearchData(); - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - - var fakeStorage = new FakeIStorage(new List { "/" ,"/test_dir" }, + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var fakeStorage = new FakeIStorage(new List { "/", "/test_dir" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); var controller = - new DiskController( _query, storageSelector, + new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()) { ControllerContext = context }; - + var result = await controller.Mkdir("/test_dir") as JsonResult; var list = result?.Value as List; - Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported,list?.FirstOrDefault()?.Status); + Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, + list?.FirstOrDefault()?.Status); } - - + + [TestMethod] public async Task Mkdir_BadRequest() { - var context = new ControllerContext - { - HttpContext = new DefaultHttpContext() - }; - - var fakeStorage = new FakeIStorage(new List { "/" ,"/test_dir" }, + var context = new ControllerContext { HttpContext = new DefaultHttpContext() }; + + var fakeStorage = new FakeIStorage(new List { "/", "/test_dir" }, new List { _createAnImage.DbPath }); var storageSelector = new FakeSelectorStorage(fakeStorage); var controller = - new DiskController( _query, storageSelector, + new DiskController(_query, storageSelector, new FakeIWebSocketConnectionsService(), new FakeINotificationQuery()) { ControllerContext = context }; await controller.Mkdir(string.Empty); - - Assert.AreEqual(400,context.HttpContext.Response.StatusCode); + + Assert.AreEqual(400, context.HttpContext.Response.StatusCode); } } } diff --git a/starsky/starskytest/Controllers/ExportControllerTest.cs b/starsky/starskytest/Controllers/ExportControllerTest.cs index 11dc80b701..056e3cdba0 100644 --- a/starsky/starskytest/Controllers/ExportControllerTest.cs +++ b/starsky/starskytest/Controllers/ExportControllerTest.cs @@ -32,7 +32,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/GeoControllerTest.cs b/starsky/starskytest/Controllers/GeoControllerTest.cs index 8d15bea800..00b35f3421 100644 --- a/starsky/starskytest/Controllers/GeoControllerTest.cs +++ b/starsky/starskytest/Controllers/GeoControllerTest.cs @@ -23,7 +23,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/HealthControllerTest.cs b/starsky/starskytest/Controllers/HealthControllerTest.cs index 49c6967e1b..055103a9d6 100644 --- a/starsky/starskytest/Controllers/HealthControllerTest.cs +++ b/starsky/starskytest/Controllers/HealthControllerTest.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.Controllers; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; using starskytest.FakeMocks; namespace starskytest.Controllers diff --git a/starsky/starskytest/Controllers/ImportControllerTest.cs b/starsky/starskytest/Controllers/ImportControllerTest.cs index 35a2b7ade4..a1f3a14eca 100644 --- a/starsky/starskytest/Controllers/ImportControllerTest.cs +++ b/starsky/starskytest/Controllers/ImportControllerTest.cs @@ -29,7 +29,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/IndexControllerTest.cs b/starsky/starskytest/Controllers/IndexControllerTest.cs index d0e3b66278..4c9e568afe 100644 --- a/starsky/starskytest/Controllers/IndexControllerTest.cs +++ b/starsky/starskytest/Controllers/IndexControllerTest.cs @@ -14,7 +14,7 @@ using starsky.foundation.database.Query; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; using starskytest.FakeMocks; namespace starskytest.Controllers @@ -30,7 +30,7 @@ public IndexControllerTest() .AddMemoryCache() .BuildServiceProvider(); var memoryCache = provider.GetService(); - + var builderDb = new DbContextOptionsBuilder(); builderDb.UseInMemoryDatabase("test"); var options = builderDb.Options; @@ -41,7 +41,7 @@ public IndexControllerTest() private async Task InsertSearchData() { - if (string.IsNullOrEmpty(await _query.GetSubPathByHashAsync("home0012304590"))) + if ( string.IsNullOrEmpty(await _query.GetSubPathByHashAsync("home0012304590")) ) { await _query.AddItemAsync(new FileIndexItem { @@ -50,13 +50,11 @@ await _query.AddItemAsync(new FileIndexItem FileHash = "home0012304590", ColorClass = ColorClassParser.Color.Winner // 1 }); - + // There must be a parent folder await _query.AddItemAsync(new FileIndexItem { - FileName = "homecontrollertest", - ParentDirectory = "", - IsDirectory = true + FileName = "homecontrollertest", ParentDirectory = "", IsDirectory = true }); } } @@ -68,24 +66,24 @@ public async Task HomeControllerIndexDetailViewTest() var controller = new IndexController(_query, new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = controller.Index("/homecontrollertest/hi.jpg") as JsonResult; - Assert.AreNotEqual(null,actionResult); + Assert.AreNotEqual(null, actionResult); var jsonCollection = actionResult?.Value as DetailView; - Assert.AreEqual("home0012304590",jsonCollection?.FileIndexItem?.FileHash); + Assert.AreEqual("home0012304590", jsonCollection?.FileIndexItem?.FileHash); } [TestMethod] public async Task HomeControllerIndexIndexViewModelTest() { await InsertSearchData(); - var controller = new IndexController(_query,new AppSettings()); + var controller = new IndexController(_query, new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = controller.Index("/homecontrollertest") as JsonResult; - Assert.AreNotEqual(null,actionResult); + Assert.AreNotEqual(null, actionResult); var jsonCollection = actionResult?.Value as ArchiveViewModel; - Assert.AreEqual("home0012304590",jsonCollection? + Assert.AreEqual("home0012304590", jsonCollection? .FileIndexItems.FirstOrDefault()?.FileHash); } - + [TestMethod] [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public void HomeControllerIndexIndexViewModel_SlashPage_Test() @@ -93,55 +91,55 @@ public void HomeControllerIndexIndexViewModel_SlashPage_Test() var fakeQuery = new FakeIQuery(new List { new FileIndexItem("/") { IsDirectory = true }, - new FileIndexItem("/test.jpg"){Tags = "test", FileHash = "test"} + new FileIndexItem("/test.jpg") { Tags = "test", FileHash = "test" } }); - - var controller = new IndexController(fakeQuery,new AppSettings()); + + var controller = new IndexController(fakeQuery, new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = controller.Index("/") as JsonResult; - Assert.AreNotEqual(null,actionResult); + Assert.AreNotEqual(null, actionResult); var jsonCollection = actionResult?.Value as ArchiveViewModel; - Assert.AreEqual("test",jsonCollection?.FileIndexItems.FirstOrDefault()?.FileHash); + Assert.AreEqual("test", jsonCollection?.FileIndexItems.FirstOrDefault()?.FileHash); } - + [TestMethod] public void HomeControllerIndexIndexViewModel_EmptyStringPage_Test() { var fakeQuery = new FakeIQuery(new List { new FileIndexItem("/") { IsDirectory = true }, - new FileIndexItem("/test.jpg"){Tags = "test", FileHash = "test"} + new FileIndexItem("/test.jpg") { Tags = "test", FileHash = "test" } }); - - var controller = new IndexController(fakeQuery,new AppSettings()); + + var controller = new IndexController(fakeQuery, new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = controller.Index(string.Empty) as JsonResult; - Assert.AreNotEqual(null,actionResult); + Assert.AreNotEqual(null, actionResult); var jsonCollection = actionResult?.Value as ArchiveViewModel; - Assert.AreEqual("test",jsonCollection?.FileIndexItems.FirstOrDefault()?.FileHash); + Assert.AreEqual("test", jsonCollection?.FileIndexItems.FirstOrDefault()?.FileHash); } [TestMethod] public void HomeControllerIndex404Test() { - var controller = new IndexController(_query,new AppSettings()); + var controller = new IndexController(_query, new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); // Act var actionResult = controller.Index("/not-found-test") as JsonResult; Assert.AreEqual("not found", actionResult?.Value); } - + [TestMethod] public async Task Index_NoItem_Give_Success_OnHome() { await InsertSearchData(); - var controller = new IndexController(new FakeIQuery(),new AppSettings()); + var controller = new IndexController(new FakeIQuery(), new AppSettings()); controller.ControllerContext.HttpContext = new DefaultHttpContext(); var actionResult = controller.Index() as JsonResult; - Assert.AreNotEqual(null,actionResult); + Assert.AreNotEqual(null, actionResult); var jsonCollection = actionResult?.Value as ArchiveViewModel; - Assert.AreEqual(0,jsonCollection?.FileIndexItems.Count()); + Assert.AreEqual(0, jsonCollection?.FileIndexItems.Count()); } } } diff --git a/starsky/starskytest/Controllers/MetaReplaceControllerTest.cs b/starsky/starskytest/Controllers/MetaReplaceControllerTest.cs index cd3edda4c8..f6dc2324fa 100644 --- a/starsky/starskytest/Controllers/MetaReplaceControllerTest.cs +++ b/starsky/starskytest/Controllers/MetaReplaceControllerTest.cs @@ -32,7 +32,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/MetaUpdateControllerTest.cs b/starsky/starskytest/Controllers/MetaUpdateControllerTest.cs index 430b1a251e..1b7902d5d9 100644 --- a/starsky/starskytest/Controllers/MetaUpdateControllerTest.cs +++ b/starsky/starskytest/Controllers/MetaUpdateControllerTest.cs @@ -34,7 +34,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/MetricsDebugControllerTest.cs b/starsky/starskytest/Controllers/MetricsDebugControllerTest.cs index b5cef747cb..660c246ab1 100644 --- a/starsky/starskytest/Controllers/MetricsDebugControllerTest.cs +++ b/starsky/starskytest/Controllers/MetricsDebugControllerTest.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.Controllers; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; using starskytest.FakeMocks; namespace starskytest.Controllers; @@ -17,6 +17,6 @@ public void MetricsDebugController_Index_test() var resultValue = jsonResult!.Value as MetricsDebugViewModel; Assert.IsNotNull(jsonResult); - Assert.AreEqual( 1d, resultValue?.CpuUsageMean); + Assert.AreEqual(1d, resultValue?.CpuUsageMean); } } diff --git a/starsky/starskytest/Controllers/PublishControllerTest.cs b/starsky/starskytest/Controllers/PublishControllerTest.cs index ed0f91bf75..df45c106be 100644 --- a/starsky/starskytest/Controllers/PublishControllerTest.cs +++ b/starsky/starskytest/Controllers/PublishControllerTest.cs @@ -19,7 +19,6 @@ using starsky.foundation.writemeta.Interfaces; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { diff --git a/starsky/starskytest/Controllers/TrashControllerTest.cs b/starsky/starskytest/Controllers/TrashControllerTest.cs index f19ea3f9cf..b0f18a4790 100644 --- a/starsky/starskytest/Controllers/TrashControllerTest.cs +++ b/starsky/starskytest/Controllers/TrashControllerTest.cs @@ -17,9 +17,9 @@ public async Task TrashControllerTest_BadInput() var controller = new TrashController( new FakeIMoveToTrashService(new List())); var result = await controller.TrashMoveAsync(null!, true) as BadRequestObjectResult; - Assert.AreEqual(400,result?.StatusCode); + Assert.AreEqual(400, result?.StatusCode); } - + [TestMethod] public async Task TrashControllerTest_NotFound() { @@ -27,35 +27,21 @@ public async Task TrashControllerTest_NotFound() new FakeIMoveToTrashService(new List())); var result = await controller.TrashMoveAsync("/test.jpg", true) as JsonResult; var resultValue = result?.Value as List; - + Assert.AreEqual(1, resultValue?.Count); } - + [TestMethod] public async Task TrashControllerTest_Ok() { var controller = new TrashController( - new FakeIMoveToTrashService(new List{new FileIndexItem("/test.jpg") + new FakeIMoveToTrashService(new List { - Status = FileIndexItem.ExifStatus.Ok - }})); + new FileIndexItem("/test.jpg") { Status = FileIndexItem.ExifStatus.Ok } + })); var result = await controller.TrashMoveAsync("/test.jpg", true) as JsonResult; var resultValue = result?.Value as List; - + Assert.AreEqual(1, resultValue?.Count); } - - [TestMethod] - public void DetectToUseSystemTrash_Ok() - { - var controller = new TrashController( - new FakeIMoveToTrashService(new List())); - - var result = controller.DetectToUseSystemTrash() as JsonResult; - - var tryParseResult = bool.TryParse(result?.Value?.ToString(), out var resultValue); - - Assert.AreEqual(true, tryParseResult); - Assert.AreEqual(true, resultValue); - } } diff --git a/starsky/starskytest/Controllers/UploadControllerTest.cs b/starsky/starskytest/Controllers/UploadControllerTest.cs index 6a4daa761d..782b803c55 100644 --- a/starsky/starskytest/Controllers/UploadControllerTest.cs +++ b/starsky/starskytest/Controllers/UploadControllerTest.cs @@ -26,7 +26,6 @@ using starsky.foundation.worker.Services; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.Controllers { @@ -59,20 +58,20 @@ public UploadControllerTest() services.AddSingleton(new ConfigurationBuilder().Build()); // random config var createAnImage = new CreateAnImage(); - _appSettings = new AppSettings { - TempFolder = createAnImage.BasePath - }; - _query = new Query(context, _appSettings, scopeFactory, new FakeIWebLogger(), memoryCache); + _appSettings = new AppSettings { TempFolder = createAnImage.BasePath }; + _query = new Query(context, _appSettings, scopeFactory, new FakeIWebLogger(), + memoryCache); + + _iStorage = new FakeIStorage(new List { "/", "/test" }, + new List { createAnImage.DbPath }, + new List { CreateAnImage.Bytes.ToArray() }); - _iStorage = new FakeIStorage(new List{"/","/test"}, - new List{createAnImage.DbPath}, - new List{CreateAnImage.Bytes.ToArray()}); - var selectorStorage = new FakeSelectorStorage(_iStorage); _import = new Import(selectorStorage, _appSettings, new FakeIImportQuery(), - new FakeExifTool(_iStorage,_appSettings), _query, new ConsoleWrapper(), - new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), new FakeIThumbnailQuery(), memoryCache); + new FakeExifTool(_iStorage, _appSettings), _query, new ConsoleWrapper(), + new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), + new FakeIThumbnailQuery(), memoryCache); // Start using dependency injection // Add random config to dependency injection @@ -86,10 +85,10 @@ public UploadControllerTest() // build the service var serviceProvider = services.BuildServiceProvider(); // get the service - + serviceProvider.GetRequiredService(); } - + /// /// Add the file in the underlying request object. /// @@ -101,8 +100,9 @@ private static ControllerContext RequestWithFile(byte[]? bytes = null) var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Append("Content-Type", "application/octet-stream"); httpContext.Request.Body = new MemoryStream(bytes); - - var actionContext = new ActionContext(httpContext, new RouteData(), new ControllerActionDescriptor()); + + var actionContext = new ActionContext(httpContext, new RouteData(), + new ControllerActionDescriptor()); return new ControllerContext(actionContext); } @@ -110,24 +110,25 @@ private static ControllerContext RequestWithFile(byte[]? bytes = null) public async Task UploadToFolder_NoToHeader_BadRequest() { var controller = - new UploadController(_import, _appSettings, - new FakeSelectorStorage(new FakeIStorage()), _query, + new UploadController(_import, _appSettings, + new FakeSelectorStorage(new FakeIStorage()), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), - new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) + new FakeIMetaExifThumbnailService(), + new FakeIMetaUpdateStatusThumbnailService()) { - ControllerContext = {HttpContext = new DefaultHttpContext()} + ControllerContext = { HttpContext = new DefaultHttpContext() } }; - - var actionResult = await controller.UploadToFolder()as BadRequestObjectResult; - - Assert.AreEqual(400,actionResult?.StatusCode); + + var actionResult = await controller.UploadToFolder() as BadRequestObjectResult; + + Assert.AreEqual(400, actionResult?.StatusCode); } - + [TestMethod] public async Task UploadToFolder_DefaultFlow() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -135,28 +136,29 @@ public async Task UploadToFolder_DefaultFlow() }; const string toPlaceSubPath = "/yes01.jpg"; - - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header - var actionResult = await controller.UploadToFolder() as JsonResult; + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header + + var actionResult = await controller.UploadToFolder() as JsonResult; var list = actionResult?.Value as List; - Assert.AreEqual( ImportStatus.Ok, list?.FirstOrDefault()?.Status); + Assert.AreEqual(ImportStatus.Ok, list?.FirstOrDefault()?.Status); var fileSystemResult = _iStorage.ExistFile(toPlaceSubPath); Assert.IsTrue(fileSystemResult); var queryResult = _query.SingleItem(toPlaceSubPath); - Assert.AreEqual("Sony",queryResult?.FileIndexItem?.Make); + Assert.AreEqual("Sony", queryResult?.FileIndexItem?.Make); await _query.RemoveItemAsync(queryResult?.FileIndexItem!); } - + [TestMethod] public async Task UploadToFolder_DefaultFlow_ColorClass() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -164,30 +166,31 @@ public async Task UploadToFolder_DefaultFlow_ColorClass() }; const string toPlaceSubPath = "/color-class01.jpg"; - - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header - var actionResult = await controller.UploadToFolder() as JsonResult; + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header + + var actionResult = await controller.UploadToFolder() as JsonResult; var list = actionResult?.Value as List; - Assert.AreEqual( ImportStatus.Ok, list?.FirstOrDefault()?.Status); + Assert.AreEqual(ImportStatus.Ok, list?.FirstOrDefault()?.Status); var fileSystemResult = _iStorage.ExistFile(toPlaceSubPath); Assert.IsTrue(fileSystemResult); var queryResult = _query.SingleItem(toPlaceSubPath); - - Assert.AreEqual("Sony",queryResult?.FileIndexItem?.Make); - Assert.AreEqual(ColorClassParser.Color.Winner,queryResult?.FileIndexItem?.ColorClass); + + Assert.AreEqual("Sony", queryResult?.FileIndexItem?.Make); + Assert.AreEqual(ColorClassParser.Color.Winner, queryResult?.FileIndexItem?.ColorClass); await _query.RemoveItemAsync(queryResult?.FileIndexItem!); } - + [TestMethod] public async Task UploadToFolder_DefaultFlow_ShouldNotOverWriteDatabase() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -199,23 +202,25 @@ public async Task UploadToFolder_DefaultFlow_ShouldNotOverWriteDatabase() // add to db await _query.AddItemAsync(new FileIndexItem(toPlaceSubPath)); - + _iStorage.CreateDirectory(toPlaceFolder); - - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header + + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header var actionResult = await controller.UploadToFolder() as JsonResult; - if ( actionResult == null ) { + if ( actionResult == null ) + { throw new WebException("actionResult should not be null"); } - + var list = actionResult.Value as List; if ( list == null ) { throw new WebException("list should not be null"); } - Assert.AreEqual( ImportStatus.Ok, list[0].Status); + Assert.AreEqual(ImportStatus.Ok, list[0].Status); var fileSystemResult = _iStorage.ExistFile(toPlaceSubPath); Assert.IsTrue(fileSystemResult); @@ -223,19 +228,19 @@ public async Task UploadToFolder_DefaultFlow_ShouldNotOverWriteDatabase() var getAllFiles = await _query.GetAllFilesAsync(toPlaceFolder); // Should not duplicate - Assert.AreEqual(1,getAllFiles.Count); - + Assert.AreEqual(1, getAllFiles.Count); + var queryResult = _query.SingleItem(toPlaceSubPath); - Assert.AreEqual("Sony",queryResult?.FileIndexItem?.Make); + Assert.AreEqual("Sony", queryResult?.FileIndexItem?.Make); await _query.RemoveItemAsync(queryResult?.FileIndexItem!); } - + [TestMethod] public async Task UploadToFolder_SidecarListShouldBeUpdated() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -246,55 +251,58 @@ public async Task UploadToFolder_SidecarListShouldBeUpdated() var toPlaceXmp = "/test_sidecar.xmp"; await _iStorage.WriteStreamAsync(new MemoryStream(new byte[1]), toPlaceXmp); - - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header + + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header await controller.UploadToFolder(); var queryResult = _query.SingleItem(toPlaceSubPath); var sidecarExtList = queryResult?.FileIndexItem?.SidecarExtensionsList.ToList(); - Assert.AreEqual(1,sidecarExtList?.Count); - Assert.AreEqual("xmp",sidecarExtList?[0]); + Assert.AreEqual(1, sidecarExtList?.Count); + Assert.AreEqual("xmp", sidecarExtList?[0]); await _query.RemoveItemAsync(queryResult?.FileIndexItem!); } - + [TestMethod] public async Task UploadToFolder_NotFound() { var controller = - new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), - new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) + new FakeIMetaExifThumbnailService(), + new FakeIMetaUpdateStatusThumbnailService()) { ControllerContext = RequestWithFile(), }; - controller.ControllerContext.HttpContext.Request.Headers["to"] = "/not-found"; //Set header + controller.ControllerContext.HttpContext.Request.Headers["to"] = + "/not-found"; //Set header - var actionResult = await controller.UploadToFolder()as NotFoundObjectResult; - - Assert.AreEqual(404,actionResult?.StatusCode); + var actionResult = await controller.UploadToFolder() as NotFoundObjectResult; + + Assert.AreEqual(404, actionResult?.StatusCode); } - + [TestMethod] public async Task UploadToFolder_UnknownFailFlow() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { ControllerContext = RequestWithFile(), }; - + controller.ControllerContext.HttpContext.Request.Headers["to"] = "/"; //Set header - var actionResult = await controller.UploadToFolder() as JsonResult; + var actionResult = await controller.UploadToFolder() as JsonResult; var list = actionResult?.Value as List; - Assert.AreEqual( ImportStatus.FileError, list?.FirstOrDefault()?.Status); + Assert.AreEqual(ImportStatus.FileError, list?.FirstOrDefault()?.Status); } [TestMethod] @@ -302,9 +310,9 @@ public void GetParentDirectoryFromRequestHeader_InputToAsSubPath() { var controllerContext = RequestWithFile(); controllerContext.HttpContext.Request.Headers.Append("to", "/test.jpg"); - - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -314,15 +322,15 @@ public void GetParentDirectoryFromRequestHeader_InputToAsSubPath() var result = controller.GetParentDirectoryFromRequestHeader(); Assert.AreEqual("/", result); } - + [TestMethod] public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_TestFolder() { var controllerContext = RequestWithFile(); controllerContext.HttpContext.Request.Headers.Append("to", "/test/test.jpg"); - - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -332,15 +340,15 @@ public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_TestFolder() var result = controller.GetParentDirectoryFromRequestHeader(); Assert.AreEqual("/test", result); } - + [TestMethod] public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_TestDirectFolder() { var controllerContext = RequestWithFile(); controllerContext.HttpContext.Request.Headers.Append("to", "/test/"); - - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -350,7 +358,7 @@ public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_TestDirectFolde var result = controller.GetParentDirectoryFromRequestHeader(); Assert.AreEqual("/test", result); } - + [TestMethod] public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_NonExistFolder() { @@ -358,18 +366,19 @@ public void GetParentDirectoryFromRequestHeader_InputToAsSubPath_NonExistFolder( controllerContext.HttpContext.Request.Headers.Append("to", "/non-exist/test.jpg"); var controller = - new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), - new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) + new FakeIMetaExifThumbnailService(), + new FakeIMetaUpdateStatusThumbnailService()) { ControllerContext = controllerContext }; - + var result = controller.GetParentDirectoryFromRequestHeader(); Assert.IsNull(result); } - + /// /// Add the file in the underlying request object. /// @@ -379,16 +388,17 @@ private static ControllerContext RequestWithSidecar() var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Append("Content-Type", "application/octet-stream"); httpContext.Request.Body = new MemoryStream(CreateAnXmp.Bytes.ToArray()); - - var actionContext = new ActionContext(httpContext, new RouteData(), new ControllerActionDescriptor()); + + var actionContext = new ActionContext(httpContext, new RouteData(), + new ControllerActionDescriptor()); return new ControllerContext(actionContext); } - + [TestMethod] public async Task UploadToFolderSidecarFile_DefaultFlow() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -396,20 +406,22 @@ public async Task UploadToFolderSidecarFile_DefaultFlow() }; var toPlaceSubPath = "/yes01.xmp"; - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header - var actionResult = await controller.UploadToFolderSidecarFile() as JsonResult; + var actionResult = await controller.UploadToFolderSidecarFile() as JsonResult; var list = actionResult?.Value as List; Assert.AreEqual(toPlaceSubPath, list?.FirstOrDefault()); } - + [TestMethod] public async Task UploadToFolderSidecarFile_UpdateMainItemWithSidecarRef() { // it should add a reference to the main item - var controller = new UploadController(_import, new AppSettings{UseDiskWatcher = false}, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, + new AppSettings { UseDiskWatcher = false }, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -419,23 +431,24 @@ public async Task UploadToFolderSidecarFile_UpdateMainItemWithSidecarRef() var dngSubPath = "/UploadToFolderSidecarFile.dng"; await _query.AddItemAsync( new FileIndexItem(dngSubPath)); - + var toPlaceSubPath = "/UploadToFolderSidecarFile.xmp"; - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header await controller.UploadToFolderSidecarFile(); var queryResult = await _query.GetObjectByFilePathAsync(dngSubPath); var sidecarExtList = queryResult?.SidecarExtensionsList.ToList(); - Assert.AreEqual(1,sidecarExtList?.Count); - Assert.AreEqual("xmp",sidecarExtList?[0]); + Assert.AreEqual(1, sidecarExtList?.Count); + Assert.AreEqual("xmp", sidecarExtList?[0]); } - + [TestMethod] public async Task UploadToFolderSidecarFile_NoXml_SoIgnore() { - var controller = new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + var controller = new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) { @@ -443,47 +456,52 @@ public async Task UploadToFolderSidecarFile_NoXml_SoIgnore() }; var toPlaceSubPath = "/yes01.xmp"; - controller.ControllerContext.HttpContext.Request.Headers["to"] = toPlaceSubPath; //Set header + controller.ControllerContext.HttpContext.Request.Headers["to"] = + toPlaceSubPath; //Set header - var actionResult = await controller.UploadToFolderSidecarFile() as JsonResult; + var actionResult = await controller.UploadToFolderSidecarFile() as JsonResult; var list = actionResult?.Value as List; Assert.AreEqual(0, list?.Count); } - + [TestMethod] public async Task UploadToFolderSidecarFile_NotFound() { var controller = - new UploadController(_import, _appSettings, - new FakeSelectorStorage(_iStorage), _query, + new UploadController(_import, _appSettings, + new FakeSelectorStorage(_iStorage), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), - new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) + new FakeIMetaExifThumbnailService(), + new FakeIMetaUpdateStatusThumbnailService()) { ControllerContext = RequestWithFile(), }; - controller.ControllerContext.HttpContext.Request.Headers["to"] = "/not-found"; //Set header + controller.ControllerContext.HttpContext.Request.Headers["to"] = + "/not-found"; //Set header + + var actionResult = await controller.UploadToFolderSidecarFile() as NotFoundObjectResult; - var actionResult = await controller.UploadToFolderSidecarFile()as NotFoundObjectResult; - - Assert.AreEqual(404,actionResult?.StatusCode); + Assert.AreEqual(404, actionResult?.StatusCode); } - + [TestMethod] public async Task UploadToFolderSidecarFile_NoToHeader_BadRequest() { var controller = - new UploadController(_import, _appSettings, - new FakeSelectorStorage(new FakeIStorage()), _query, + new UploadController(_import, _appSettings, + new FakeSelectorStorage(new FakeIStorage()), _query, new FakeIRealtimeConnectionsService(), new FakeIWebLogger(), - new FakeIMetaExifThumbnailService(), new FakeIMetaUpdateStatusThumbnailService()) + new FakeIMetaExifThumbnailService(), + new FakeIMetaUpdateStatusThumbnailService()) { - ControllerContext = {HttpContext = new DefaultHttpContext()} + ControllerContext = { HttpContext = new DefaultHttpContext() } }; - - var actionResult = await controller.UploadToFolderSidecarFile()as BadRequestObjectResult; - - Assert.AreEqual(400,actionResult?.StatusCode); + + var actionResult = + await controller.UploadToFolderSidecarFile() as BadRequestObjectResult; + + Assert.AreEqual(400, actionResult?.StatusCode); } } } diff --git a/starsky/starskytest/FakeCreateAn/CreateAnExifToolWindows.cs b/starsky/starskytest/FakeCreateAn/CreateAnExifToolWindows.cs index b78869d55a..16db130987 100644 --- a/starsky/starskytest/FakeCreateAn/CreateAnExifToolWindows.cs +++ b/starsky/starskytest/FakeCreateAn/CreateAnExifToolWindows.cs @@ -287,9 +287,16 @@ public static class CreateAnExifToolWindows "ABAAJAAAAAAAAAAggKSBAAAAAGV4aWZ0b29sKC1rKS5leGUKACAAAAAAAAEAGAAAzW9bwS3WAQA0dNHB" + "LdYBAHu8iMEt1gFQSwUGAAAAAAEAAQBiAAAADUEAAAAA"; + /// + /// This is a zip file + /// public static readonly ImmutableArray Bytes = - Base64Helper.TryParse(ImageExifToolZipWindows).ToImmutableArray(); + [.. Base64Helper.TryParse(ImageExifToolZipWindows)]; + + /// + /// File hash to check the content of the zip file + /// public static readonly string Sha1 = "0da554d4cf5f4c15591da109ae070742ecfceb65"; } } diff --git a/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyUnixBash.cs b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyUnixBash.cs new file mode 100644 index 0000000000..f839eb4fdf --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyUnixBash.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Reflection; + +namespace starskytest.FakeCreateAn.CreateFakeStarskyExe; + +public class CreateFakeStarskyUnixBash +{ + public CreateFakeStarskyUnixBash() + { + var dirName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if ( string.IsNullOrEmpty(dirName) ) return; + var parentFolder = Path.Combine(dirName, "FakeCreateAn", + "CreateFakeStarskyExe"); + var path = Path.Combine(parentFolder, "starsky"); + FullFilePath = path; + StarskyDotStarskyPath = Path.Combine(parentFolder, "starsky.starsky"); + if ( !File.Exists(FullFilePath) || !File.Exists(StarskyDotStarskyPath) ) + { + throw new Exception("missing starsky or starsky.starsky file in " + parentFolder); + } + } + + public string StarskyDotStarskyPath { get; set; } = string.Empty; + + public string FullFilePath { get; set; } = string.Empty; + + /// + /// ApplicationUrl is the same as FullFilePath + /// + public string ApplicationUrl => FullFilePath; +} diff --git a/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyWindowsExe.cs b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyWindowsExe.cs new file mode 100644 index 0000000000..fe541fcf62 --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/CreateFakeStarskyWindowsExe.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Reflection; + +namespace starskytest.FakeCreateAn.CreateFakeStarskyExe; + +public class CreateFakeStarskyWindowsExe +{ + public CreateFakeStarskyWindowsExe() + { + var dirName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if ( string.IsNullOrEmpty(dirName) ) return; + var parentFolder = Path.Combine(dirName, "FakeCreateAn", + "CreateFakeStarskyExe"); + var path = Path.Combine(parentFolder, "starsky.exe"); + FullFilePath = path; + StarskyDotStarskyPath = Path.Combine(parentFolder, "starsky.starsky"); + if ( !File.Exists(FullFilePath) || !File.Exists(StarskyDotStarskyPath) ) + { + throw new Exception("missing starsky.exe or starsky.starsky file in " + parentFolder); + } + } + + public string FullFilePath { get; set; } = string.Empty; + public string StarskyDotStarskyPath { get; set; } = string.Empty; +} diff --git a/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky-macos.zip b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky-macos.zip new file mode 100644 index 0000000000..29f854c19b Binary files /dev/null and b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky-macos.zip differ diff --git a/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky.starsky b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky.starsky new file mode 100644 index 0000000000..1cddf9dbab --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateFakeStarskyExe/starsky.starsky @@ -0,0 +1 @@ +# no content \ No newline at end of file diff --git a/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.cpp-reference b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.cpp-reference new file mode 100644 index 0000000000..afb21bb89a --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.cpp-reference @@ -0,0 +1,41 @@ +#include "MacOsStub.h" + +#ifdef __cplusplus +extern "C" { +#endif + + __declspec(dllexport) void* CFStringCreateWithBytes(void* allocator, void* buffer, long bufferLength, void* encoding, bool isExternalRepresentation) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void* CreateCfString(const char* aString) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void* CreateCfArray(void** objects, long numObjects) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void CFRelease(void* handle) { + // Stub implementation + } + + __declspec(dllexport) void* objc_getClass(const char* name) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void* NSSelectorFromString(void* cfstr) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void* objc_msgSend_retIntPtr(void* target, void* selector) { + return nullptr; // Stub implementation + } + + __declspec(dllexport) void objc_msgSend_retVoid_IntPtr_IntPtr(void* target, void* selector, void* param1, void* param2) { + // Stub implementation + } + +#ifdef __cplusplus +} +#endif diff --git a/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.dll b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.dll new file mode 100644 index 0000000000..3bb47bb60b Binary files /dev/null and b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.dll differ diff --git a/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.h-reference b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.h-reference new file mode 100644 index 0000000000..ac0e48291b --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/MacOsStub.h-reference @@ -0,0 +1,23 @@ +#ifndef MACOS_STUB_H +#define MACOS_STUB_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + __declspec(dllexport) void* CFStringCreateWithBytes(void* allocator, void* buffer, long bufferLength, void* encoding, bool isExternalRepresentation); + __declspec(dllexport) void* CreateCfString(const char* aString); + __declspec(dllexport) void* CreateCfArray(void** objects, long numObjects); + __declspec(dllexport) void CFRelease(void* handle); + __declspec(dllexport) void* objc_getClass(const char* name); + __declspec(dllexport) void* NSSelectorFromString(void* cfstr); + __declspec(dllexport) void* objc_msgSend_retIntPtr(void* target, void* selector); + __declspec(dllexport) void objc_msgSend_retVoid_IntPtr_IntPtr(void* target, void* selector, void* param1, void* param2); + +#ifdef __cplusplus +} +#endif + +#endif // MACOS_STUB_H diff --git a/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/readme.md b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/readme.md new file mode 100644 index 0000000000..16cf7a5c96 --- /dev/null +++ b/starsky/starskytest/FakeCreateAn/CreateOpenApplicationNative.reference/readme.md @@ -0,0 +1,29 @@ +For reference only +**Not working correctly yet** + +Visual studio installer -> Modify +-> Desktop Enviorment with C++ + +To compile the C++ code using the command line on Windows, you can use the Visual Studio Command +Prompt, which sets up the necessary environment variables and paths for using the Visual C++ +compiler (cl.exe) and other tools. Here's how you can compile the code: + + Open the Visual Studio Command Prompt: + Press the Windows key and type "Visual Studio Command Prompt". + Open the command prompt with the appropriate version of Visual Studio you're using (e.g., Developer Command Prompt for Visual Studio 2019). + + Navigate to the directory containing your C++ source files: + Use the cd command to change to the directory where MacOsStub.cpp and MacOsStub.h are located. + + Compile the code: + Use the cl command to compile the C++ code and generate the DLL. Here's a basic command: + + cl /EHsc /LD /arch=arm64 MacOsStub.cpp + + /EHsc enables standard C++ exception handling. + /LD specifies that the output should be a DLL. + MacOsStub.cpp is the name of your C++ source file. + +Verify the output: + + After a successful compilation, you should find the compiled DLL in the same directory as your source files. \ No newline at end of file diff --git a/starsky/starskytest/Models/FakeExifTool.cs b/starsky/starskytest/FakeMocks/FakeExifTool.cs similarity index 60% rename from starsky/starskytest/Models/FakeExifTool.cs rename to starsky/starskytest/FakeMocks/FakeExifTool.cs index 074bcf44c7..5cb7ede671 100644 --- a/starsky/starskytest/Models/FakeExifTool.cs +++ b/starsky/starskytest/FakeMocks/FakeExifTool.cs @@ -8,7 +8,7 @@ using starsky.foundation.storage.Services; using starsky.foundation.writemeta.Interfaces; -namespace starskytest.Models +namespace starskytest.FakeMocks { public sealed class FakeExifTool : IExifToolHostStorage { @@ -19,13 +19,16 @@ public FakeExifTool(IStorage iStorage, AppSettings _) { _iStorage = iStorage; } - - public const string XmpInjection = "" + - "\n\n" + - "\n \n \n " + - " \n " + "test\n \n \n \n\n" + - " \n " + - "kamer\n \n\n\n"; + + public const string XmpInjection = + "" + + "\n\n" + + "\n \n \n " + + " \n " + + "test\n \n \n \n\n" + + " \n " + + "kamer\n \n\n\n"; public async Task WriteTagsAsync(string subPath, string command) { @@ -36,10 +39,12 @@ public async Task WriteTagsAsync(string subPath, string command) var stream = StringToStreamHelper.StringToStream(XmpInjection); await _iStorage.WriteStreamAsync(stream, subPath); } + return true; } - public async Task> WriteTagsAndRenameThumbnailAsync(string subPath, string? beforeFileHash, + public async Task> WriteTagsAndRenameThumbnailAsync( + string subPath, string? beforeFileHash, string command, CancellationToken cancellationToken = default) { Console.WriteLine("Fake ExifTool + " + subPath + " " + command); @@ -49,8 +54,8 @@ public async Task> WriteTagsAndRenameThumbnailAsync(s var stream = StringToStreamHelper.StringToStream(XmpInjection); await _iStorage.WriteStreamAsync(stream, subPath); } - - var newFileHash = (await new FileHash(_iStorage).GetHashCodeAsync(subPath)).Key; + + var newFileHash = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) ).Key; return new KeyValuePair(true, newFileHash); } diff --git a/starsky/starskytest/FakeMocks/FakeIImportQuery.cs b/starsky/starskytest/FakeMocks/FakeIImportQuery.cs index 0a5d147556..4fdd34c75c 100644 --- a/starsky/starskytest/FakeMocks/FakeIImportQuery.cs +++ b/starsky/starskytest/FakeMocks/FakeIImportQuery.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using starsky.foundation.database.Data; @@ -20,7 +21,7 @@ public FakeIImportQuery(List exist, bool isConnection = true) { _exist = exist; _isConnection = isConnection; - if ( exist == null ) _exist = new List(); + if ( exist == null! ) _exist = new List(); } public FakeIImportQuery() @@ -36,6 +37,8 @@ public FakeIImportQuery() /// /// /// + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + [SuppressMessage("Usage", "IDE0060:Remove unused parameter")] public FakeIImportQuery(IServiceScopeFactory scopeFactory, IConsole console, IWebLogger logger, ApplicationDbContext? dbContext = null) { diff --git a/starsky/starskytest/FakeMocks/FakeIOpenApplicationNativeService.cs b/starsky/starskytest/FakeMocks/FakeIOpenApplicationNativeService.cs new file mode 100644 index 0000000000..92dc55a5dc --- /dev/null +++ b/starsky/starskytest/FakeMocks/FakeIOpenApplicationNativeService.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using starsky.foundation.native.OpenApplicationNative; +using starsky.foundation.native.OpenApplicationNative.Interfaces; + +namespace starskytest.FakeMocks; + +public class FakeIOpenApplicationNativeService : IOpenApplicationNativeService +{ + private readonly List _fullFilePaths; + private readonly string _applicationUrl; + private readonly bool _isSupported; + + public FakeIOpenApplicationNativeService(List fullPaths, string applicationUrl, + bool isSupported = true) + { + _fullFilePaths = fullPaths; + _applicationUrl = applicationUrl; + _isSupported = isSupported; + } + + public string FindPath(List fullPaths) + { + var fullFilePath = string.Empty; + foreach ( var path in fullPaths ) + { + var findPath = _fullFilePaths.Find(p => p == path); + if ( findPath != null ) + { + fullFilePath = findPath; + } + } + + return fullFilePath; + } + + public bool DetectToUseOpenApplication() + { + return _isSupported; + } + + public bool? OpenApplicationAtUrl(List<(string, string)> fullPathAndApplicationUrl) + { + var filesByApplicationPath = + OpenApplicationNativeService + .SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl); + + var results = new List(); + foreach ( var (fullFilePaths, applicationPath) in filesByApplicationPath ) + { + results.Add(OpenApplicationAtUrl(fullFilePaths, applicationPath)); + } + + return results.TrueForAll(p => p == true); + } + + public bool? OpenApplicationAtUrl(List fullPaths, string applicationUrl) + { + var findPath = FindPath(fullPaths); + return !string.IsNullOrEmpty(findPath) && applicationUrl == _applicationUrl; + } + + public bool? OpenDefault(List fullPaths) + { + var findPath = FindPath(fullPaths); + return !string.IsNullOrEmpty(findPath); + } +} diff --git a/starsky/starskytest/FakeMocks/FakeIOpenEditorDesktopService.cs b/starsky/starskytest/FakeMocks/FakeIOpenEditorDesktopService.cs new file mode 100644 index 0000000000..a7d00f0090 --- /dev/null +++ b/starsky/starskytest/FakeMocks/FakeIOpenEditorDesktopService.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using starsky.feature.desktop.Interfaces; +using starsky.feature.desktop.Models; +using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; + +namespace starskytest.FakeMocks; + +public class FakeIOpenEditorDesktopService : IOpenEditorDesktopService +{ + private readonly bool _isEnabled; + + public FakeIOpenEditorDesktopService() + { + _isEnabled = true; + } + + public FakeIOpenEditorDesktopService(bool isEnabled) + { + _isEnabled = isEnabled; + } + + public bool OpenAmountConfirmationChecker(string f) + { + return true; + } + + public bool IsEnabled() + { + return _isEnabled; + } + + public async Task<(bool?, string, List)> OpenAsync(string f, + bool collections) + { + await Task.Yield(); + + var list = new List + { + new PathImageFormatExistsAppPathModel + { + AppPath = "test", + Status = FileIndexItem.ExifStatus.Ok, + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg, + SubPath = "/test.jpg", + FullFilePath = "/test.jpg" + } + }; + + return ( _isEnabled, "Opened", list ); + } +} diff --git a/starsky/starskytest/FakeMocks/FakeIOpenEditorPreflight.cs b/starsky/starskytest/FakeMocks/FakeIOpenEditorPreflight.cs new file mode 100644 index 0000000000..f11fa9207f --- /dev/null +++ b/starsky/starskytest/FakeMocks/FakeIOpenEditorPreflight.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using starsky.feature.desktop.Interfaces; +using starsky.feature.desktop.Models; + +namespace starskytest.FakeMocks; + +public class FakeIOpenEditorPreflight : IOpenEditorPreflight +{ + private readonly List _content; + + public FakeIOpenEditorPreflight(List content) + { + _content = content; + } + + public Task> PreflightAsync(List inputFilePaths, + bool collections) + { + return Task.FromResult(_content); + } +} diff --git a/starsky/starskytest/FakeMocks/FakeIUserManger.cs b/starsky/starskytest/FakeMocks/FakeIUserManger.cs index 1bbaa1b0ae..771cb53227 100644 --- a/starsky/starskytest/FakeMocks/FakeIUserManger.cs +++ b/starsky/starskytest/FakeMocks/FakeIUserManger.cs @@ -115,6 +115,11 @@ public Role GetRole(string credentialTypeCode, string identifier) throw new System.NotImplementedException(); } + public Task GetRoleAsync(int userId) + { + throw new System.NotImplementedException(); + } + public bool PreflightValidate(string userName, string password, string confirmPassword) { diff --git a/starsky/starskytest/FakeMocks/FakeUserManagerActiveUsers.cs b/starsky/starskytest/FakeMocks/FakeUserManagerActiveUsers.cs index 779c9c178f..19cb1d6b18 100644 --- a/starsky/starskytest/FakeMocks/FakeUserManagerActiveUsers.cs +++ b/starsky/starskytest/FakeMocks/FakeUserManagerActiveUsers.cs @@ -154,6 +154,11 @@ public Task RemoveUser(string credentialTypeCode, return Role; } + public Task GetRoleAsync(int userId) + { + return Task.FromResult(Role); + } + public bool PreflightValidate(string userName, string password, string confirmPassword) { return password != "false"; diff --git a/starsky/starskytest/Helpers/MimeHelperTest.cs b/starsky/starskytest/Helpers/MimeHelperTest.cs deleted file mode 100644 index d72b54b11c..0000000000 --- a/starsky/starskytest/Helpers/MimeHelperTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starskycore.Helpers; - -namespace starskytest.Helpers -{ - [TestClass] - public sealed class MimeHelperTest - { - [TestMethod] - public void GetMimeTypeByFileNameTestUnknown() - { - Assert.AreEqual("application/octet-stream",MimeHelper.GetMimeTypeByFileName("test.unknown")); - } - - [TestMethod] - public void GetMimeTypeByFileNameTestJpg() - { - Assert.AreEqual("image/jpeg",MimeHelper.GetMimeTypeByFileName("test.jpg")); - } - - [TestMethod] - public void GetMimeTypeByFileNameTestJpeg() - { - Assert.AreEqual("image/jpeg",MimeHelper.GetMimeTypeByFileName("test.jpeg")); - } - - [TestMethod] - public void GetMimeTypeByExtensionTest_NoExtension() - { - Assert.AreEqual("application/octet-stream",MimeHelper.GetMimeTypeByFileName(string.Empty)); - } - - [TestMethod] - public void GetMimeType_NoExtension() - { - Assert.AreEqual("application/octet-stream",MimeHelper.GetMimeType(string.Empty)); - } - - - [TestMethod] - public void GetMimeType_Jpeg() - { - Assert.AreEqual("image/jpeg",MimeHelper.GetMimeType("jpg")); - } - } -} diff --git a/starsky/starskytest/Models/Account/CredentialTypeTest.cs b/starsky/starskytest/Models/Account/CredentialTypeTest.cs deleted file mode 100644 index e89c40d593..0000000000 --- a/starsky/starskytest/Models/Account/CredentialTypeTest.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.database.Models.Account; - -namespace starskytest.Models.Account -{ - [TestClass] - public sealed class CredentialTypeTest - { - [TestMethod] - public void CredentialTypeSetup_Test() - { - var creds = new CredentialType - { - Id = 0, - Code = string.Empty, - Name = string.Empty, - Position = 0, - Credentials = new List() - }; - Assert.AreEqual(0, creds.Id); - Assert.AreEqual(0, creds.Position); - Assert.AreEqual(string.Empty, creds.Code); - - } - } -} diff --git a/starsky/starskytest/Models/Account/PermissionTest.cs b/starsky/starskytest/Models/Account/PermissionTest.cs deleted file mode 100644 index 5dcbd14757..0000000000 --- a/starsky/starskytest/Models/Account/PermissionTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.database.Models.Account; - -namespace starskytest.Models.Account -{ - [TestClass] - public sealed class PermissionTest - { - [TestMethod] - public void CredentialSetupTest() - { - var creds = new Permission - { - Id = 0, - Code = string.Empty, - Name = string.Empty, - Position = 0 - }; - Assert.AreEqual(0, creds.Id); - Assert.AreEqual(0, creds.Position); - Assert.AreEqual(string.Empty, creds.Code); - - } - } -} diff --git a/starsky/starskytest/Models/Account/RolePermissionTest.cs b/starsky/starskytest/Models/Account/RolePermissionTest.cs deleted file mode 100644 index 8b29e4e733..0000000000 --- a/starsky/starskytest/Models/Account/RolePermissionTest.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.database.Models.Account; - -namespace starskytest.Models.Account -{ - [TestClass] - public sealed class RolePermissionTest - { - [TestMethod] - public void RolePermissionSetupTest() - { - // RoleId + PermissionId - var creds = new RolePermission - { - RoleId = 0, - PermissionId = 0, - Role = new Role(), - Permission = new Permission() - }; - Assert.AreEqual(0, creds.RoleId); - Assert.AreEqual(0, creds.PermissionId); - } - } -} diff --git a/starsky/starskytest/Models/Account/UserRoleTest.cs b/starsky/starskytest/Models/Account/UserRoleTest.cs deleted file mode 100644 index 7a5f9f7fcc..0000000000 --- a/starsky/starskytest/Models/Account/UserRoleTest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.database.Models.Account; - -namespace starskytest.Models.Account -{ - [TestClass] - public sealed class UserRoleTest - { - [TestMethod] - public void UserRoleTest_SetupTest() - { - var role = new UserRole() - { - UserId = 0, - RoleId = 0, - User = new User(), - Role = new Role() - }; - Assert.AreEqual(0, role.UserId); - Assert.AreEqual(0, role.RoleId); - } - } -} diff --git a/starsky/starskytest/Models/FolderOrFileModelTest.cs b/starsky/starskytest/Models/FolderOrFileModelTest.cs deleted file mode 100644 index cba184cc58..0000000000 --- a/starsky/starskytest/Models/FolderOrFileModelTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.storage.Models; - -namespace starskytest.Models -{ - [TestClass] - public sealed class FolderOrFileModelTest - { - [TestMethod] - public void FolderOrFileModelFolderOrFileTypeListTest() - { - var ToSearchType = FolderOrFileModel.FolderOrFileTypeList.Folder; - var folderOrFileModel = new FolderOrFileModel - { - IsFolderOrFile = ToSearchType - }; - Assert.AreEqual(folderOrFileModel.IsFolderOrFile,ToSearchType); - } - - } -} diff --git a/starsky/starskytest/Models/SearchViewModelTest.cs b/starsky/starskytest/Models/SearchViewModelTest.cs deleted file mode 100644 index 2b3a9cd088..0000000000 --- a/starsky/starskytest/Models/SearchViewModelTest.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.feature.search.ViewModels; -using starsky.foundation.database.Models; - -namespace starskytest.Models -{ - [TestClass] - public sealed class SearchViewModelTest - { - [TestMethod] - public void SearchViewModel_ElapsedSeconds_Test() - { - var searchViewModel = new SearchViewModel{ElapsedSeconds = 0.0006}; - Assert.AreEqual(true, searchViewModel.ElapsedSeconds <= 0.001); - } - - [TestMethod] - public void SearchViewModel_Offset_Test() - { - var searchViewModel = new SearchViewModel(); - Assert.AreEqual(0,Math.Floor(searchViewModel.Offset)); - } - - [TestMethod] - public void PropertySearchTest() - { - var property = new FileIndexItem{Tags = "q"}.GetType().GetProperty(nameof(FileIndexItem.Tags))!; - - // not a great test - var search = SearchViewModel.PropertySearch(new SearchViewModel{SearchFor = { "q" }}, property, - "q", SearchViewModel.SearchForOptionType.Equal); - - Assert.AreEqual(0, search.CollectionsCount); - } - - [TestMethod] - public void PropertySearchStringType_DefaultCase_NullConditions1() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = null - }; - - var property = typeof(FileIndexItem).GetProperty("NotFound"); - Assert.IsNull(property); - - const string searchForQuery = "file"; - var searchType = SearchViewModel.SearchForOptionType.Equal; - - // Act - var result = SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, searchType); - - // Assert - Assert.IsNotNull(result); - Assert.IsNull(result.FileIndexItems); - } - - [TestMethod] - public void PropertySearchStringType_DefaultCase_NullConditions2() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = null - }; - - var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.Tags)); - const string searchForQuery = "file"; - var searchType = SearchViewModel.SearchForOptionType.Equal; - - // Act - var result = SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, searchType); - - // Assert - Assert.IsNotNull(result); - Assert.IsNull(result.FileIndexItems); - } - - [TestMethod] - public void PropertySearchStringType_DefaultCase_Found_Null() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = new List{new FileIndexItem("test"){LocationCity = null}} - }; - - var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.LocationCity)); - const string searchForQuery = "file"; - var searchType = SearchViewModel.SearchForOptionType.Equal; - - // Act - var result = SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, searchType); - - // Assert - Assert.IsNotNull(result); - Assert.AreEqual(0,result.FileIndexItems?.Count); - } - - [TestMethod] - public void PropertySearchStringType_DefaultCase_Found_HappyFlow() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = new List{new FileIndexItem("test"){LocationCity = "test"}} - }; - - var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.LocationCity)); - const string searchForQuery = "test"; - var searchType = SearchViewModel.SearchForOptionType.Equal; - - // Act - var result = SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, searchType); - - // Assert - Assert.IsNotNull(result); - Assert.AreEqual(1,result.FileIndexItems?.Count); - } - - [TestMethod] - public void PropertySearchBoolType_FiltersItemsByBoolProperty() - { - // Arrange - var model = new SearchViewModel(); - model.FileIndexItems = new List - { - new FileIndexItem { IsDirectory = true }, - new FileIndexItem { IsDirectory = false }, - new FileIndexItem { IsDirectory = true }, - }; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - var boolIsValue = true; - - // Act - var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); - - // Assert - Assert.AreEqual(2, result.FileIndexItems?.Count); - Assert.IsTrue(result.FileIndexItems?.Exists(item => item.IsDirectory == true)); - } - - [TestMethod] - public void PropertySearchBoolType_WithNullModel_ReturnsNullModel() - { - // Arrange - SearchViewModel? model = null; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - const bool boolIsValue = true; - - // Act - var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); - - // Assert - Assert.IsNotNull(result); - } - - [TestMethod] - public void PropertySearchBoolType_WithNullFileIndexItems_ReturnsNullFileIndexItems() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = null, - }; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - var boolIsValue = true; - - // Act - var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); - - // Assert - Assert.IsNull(result.FileIndexItems); - } - - [TestMethod] - public void PropertySearchBoolType_WithEmptyFileIndexItems_ReturnsEmptyFileIndexItems() - { - // Arrange - var model = new SearchViewModel - { - FileIndexItems = new List(), - }; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - var boolIsValue = true; - - // Act - var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); - - // Assert - Assert.IsNotNull(result.FileIndexItems); - Assert.AreEqual(0, result.FileIndexItems.Count); - } - - [TestMethod] - public void PropertySearchBoolType_WithInvalidProperty_ReturnsOriginalModel() - { - // Arrange - var model = new SearchViewModel(); - model.FileIndexItems = new List - { - new FileIndexItem { IsDirectory = true }, - }; - var property = typeof(FileIndexItem).GetProperty("NonExistentProperty"); - var boolIsValue = true; - - // Act - var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); - - // Assert - Assert.AreEqual(model, result); - } - - [TestMethod] - public void PropertySearch_WithBoolPropertyAndValidBoolValue_ReturnsFilteredModel() - { - // Arrange - var model = new SearchViewModel { FileIndexItems = new List - { - new FileIndexItem { IsDirectory = true }, - new FileIndexItem { IsDirectory = false }, - new FileIndexItem { IsDirectory = true }, - } - }; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - const string searchForQuery = "true"; - const SearchViewModel.SearchForOptionType searchType = SearchViewModel.SearchForOptionType.Equal; - - // Act - var result = SearchViewModel.PropertySearch(model, property!, searchForQuery, searchType); - - // Assert - Assert.AreEqual(2, result.FileIndexItems?.Count); - Assert.IsTrue(result.FileIndexItems?.Exists(item => item.IsDirectory == true)); - } - - [TestMethod] - public void PropertySearch_WithBoolPropertyAndInvalidBoolValue_ReturnsOriginalModel() - { - // Arrange - var model = new SearchViewModel(); - model.FileIndexItems = new List - { - new FileIndexItem { IsDirectory = true }, - new FileIndexItem { IsDirectory = false }, - }; - var property = typeof(FileIndexItem).GetProperty("IsDirectory"); - const string searchForQuery = "invalid_bool_value"; // An invalid boolean string - const SearchViewModel.SearchForOptionType searchType = SearchViewModel.SearchForOptionType.Equal; // You can set this as needed - - // Act - var result = SearchViewModel.PropertySearch(model, property!, searchForQuery, searchType); - - // Assert - CollectionAssert.AreEqual(model.FileIndexItems, result.FileIndexItems); - } - } -} diff --git a/starsky/starskytest/ViewModels/ArchiveViewModelTest.cs b/starsky/starskytest/ViewModels/ArchiveViewModelTest.cs index 9cceac0f0b..a8e841c099 100644 --- a/starsky/starskytest/ViewModels/ArchiveViewModelTest.cs +++ b/starsky/starskytest/ViewModels/ArchiveViewModelTest.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starskytest.ViewModels { @@ -13,47 +13,48 @@ public sealed class ArchiveViewModelTest [TestMethod] public void ArchiveViewModelPageTypeTest() { - var viewModel = new ArchiveViewModel(); - Assert.AreEqual( PageViewType.PageType.Archive.ToString(),viewModel.PageType); + var viewModel = new ArchiveViewModel(); + Assert.AreEqual(PageViewType.PageType.Archive.ToString(), viewModel.PageType); } - + [TestMethod] public void ArchiveViewModel_ColorClass() { var viewModel = new ArchiveViewModel { - ColorClassActiveList = new List{ColorClassParser.Color.None}, - ColorClassUsage = new List{ColorClassParser.Color.None} + ColorClassActiveList = + new List { ColorClassParser.Color.None }, + ColorClassUsage = + new List { ColorClassParser.Color.None } }; - - Assert.AreEqual(ColorClassParser.Color.None, viewModel.ColorClassActiveList.FirstOrDefault()); - Assert.AreEqual(ColorClassParser.Color.None, viewModel.ColorClassUsage.FirstOrDefault()); + + Assert.AreEqual(ColorClassParser.Color.None, + viewModel.ColorClassActiveList.FirstOrDefault()); + Assert.AreEqual(ColorClassParser.Color.None, + viewModel.ColorClassUsage.FirstOrDefault()); } - + [TestMethod] public void ArchiveViewModel_ExampleData() { var archiveViewModel = new ArchiveViewModel { FileIndexItems = new List(), - Breadcrumb = new List{"/"}, - RelativeObjects = new RelativeObjects - { - NextFilePath = "/" - }, + Breadcrumb = new List { "/" }, + RelativeObjects = new RelativeObjects { NextFilePath = "/" }, SearchQuery = "test", SubPath = "/", IsReadOnly = false, - CollectionsCount= 0, + CollectionsCount = 0, Collections = true, }; - - Assert.AreEqual("/", archiveViewModel.Breadcrumb.FirstOrDefault()); - Assert.AreEqual("/", archiveViewModel.RelativeObjects.NextFilePath); - Assert.AreEqual("test", archiveViewModel.SearchQuery); - Assert.AreEqual("/", archiveViewModel.SubPath); - Assert.AreEqual(false, archiveViewModel.IsReadOnly); - Assert.AreEqual(0, archiveViewModel.CollectionsCount); + + Assert.AreEqual("/", archiveViewModel.Breadcrumb.FirstOrDefault()); + Assert.AreEqual("/", archiveViewModel.RelativeObjects.NextFilePath); + Assert.AreEqual("test", archiveViewModel.SearchQuery); + Assert.AreEqual("/", archiveViewModel.SubPath); + Assert.AreEqual(false, archiveViewModel.IsReadOnly); + Assert.AreEqual(0, archiveViewModel.CollectionsCount); Assert.IsTrue(archiveViewModel.Collections); } } diff --git a/starsky/starskytest/ViewModels/SyncViewModelTest.cs b/starsky/starskytest/ViewModels/SyncViewModelTest.cs index 28dbbeedff..87ac0886cb 100644 --- a/starsky/starskytest/ViewModels/SyncViewModelTest.cs +++ b/starsky/starskytest/ViewModels/SyncViewModelTest.cs @@ -1,6 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models; -using starskycore.ViewModels; +using starsky.project.web.ViewModels; namespace starskytest.ViewModels { @@ -12,12 +12,11 @@ public void SyncViewModelSyncViewModelTest() { var syncViewModel = new SyncViewModel { - FilePath = "/test", - Status = FileIndexItem.ExifStatus.Ok - }; - - Assert.AreEqual("/test",syncViewModel.FilePath); - Assert.AreEqual(FileIndexItem.ExifStatus.Ok,syncViewModel.Status); + FilePath = "/test", Status = FileIndexItem.ExifStatus.Ok + }; + + Assert.AreEqual("/test", syncViewModel.FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, syncViewModel.Status); } } } diff --git a/starsky/starskytest/root/StartupTest.cs b/starsky/starskytest/root/StartupTest.cs index 8e0f108233..62bfd483ac 100644 --- a/starsky/starskytest/root/StartupTest.cs +++ b/starsky/starskytest/root/StartupTest.cs @@ -30,23 +30,26 @@ public void Startup_ConfigureServices() { IServiceCollection serviceCollection = new ServiceCollection(); // needed for: AddMetrics - IConfiguration configuration = new ConfigurationRoot(new List()); - serviceCollection.AddSingleton(configuration); - + IConfiguration configuration = + new ConfigurationRoot(new List()); + serviceCollection.AddSingleton(configuration); + // should not crash new Startup().ConfigureServices(serviceCollection); Assert.IsNotNull(serviceCollection); } - + [TestMethod] public void Startup_ConfigureServicesConfigure1() { var serviceCollection = new ServiceCollection(); serviceCollection.AddRouting(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - IConfiguration configuration = new ConfigurationRoot(new List()); - serviceCollection.AddSingleton(configuration); + serviceCollection + .AddSingleton(); + IConfiguration configuration = + new ConfigurationRoot(new List()); + serviceCollection.AddSingleton(configuration); serviceCollection.AddAuthorization(); serviceCollection.AddControllers(); serviceCollection.AddLogging(); @@ -54,19 +57,20 @@ public void Startup_ConfigureServicesConfigure1() var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProviderInterface = serviceProvider.GetRequiredService(); - + var applicationBuilder = new ApplicationBuilder(serviceProviderInterface); - IHostEnvironment env = new HostingEnvironment { EnvironmentName = Environments.Development }; + IHostEnvironment env = + new HostingEnvironment { EnvironmentName = Environments.Development }; // should not crash var startup = new Startup(); - + startup.ConfigureServices(serviceCollection); var appSettings = serviceProvider.GetRequiredService(); appSettings.UseRealtime = true; - startup.Configure(applicationBuilder, env, new FakeIApplicationLifetime()); - + startup.Configure(applicationBuilder, env); + Assert.IsNotNull(applicationBuilder); Assert.IsNotNull(env); } @@ -74,14 +78,15 @@ public void Startup_ConfigureServicesConfigure1() [SuppressMessage("ReSharper", "ReturnTypeCanBeEnumerable.Local")] private static List? GetMiddlewareInstance(IApplicationBuilder app) { - const string middlewareTypeName = "Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware"; + const string middlewareTypeName = + "Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware"; var appBuilderType = typeof(ApplicationBuilder); const BindingFlags bindingTypes1 = BindingFlags.Instance | - BindingFlags.NonPublic; + BindingFlags.NonPublic; var middlewareField = appBuilderType.GetField("_components", bindingTypes1); var components = middlewareField?.GetValue(app); - if (components != null) + if ( components != null ) { var element = components as List>; @@ -97,20 +102,21 @@ public void Startup_ConfigureServicesConfigure1() { var type = middleware.Target?.GetType(); const BindingFlags bindingTypes = BindingFlags.Instance | - BindingFlags.NonPublic | - BindingFlags.Public; + BindingFlags.NonPublic | + BindingFlags.Public; var privatePropertyInfo = type?.GetField("_args", bindingTypes); var privateFieldValue = privatePropertyInfo?.GetValue(middleware.Target) as object[]; - + status.Add(privateFieldValue); } + return status; } return null; } - + [TestMethod] public void BasicFlow_Default() { @@ -118,29 +124,32 @@ public void BasicFlow_Default() var serviceCollection = new ServiceCollection(); var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProviderInterface = serviceProvider.GetRequiredService(); - + var applicationBuilder = new ApplicationBuilder(serviceProviderInterface); var result = startup.SetupStaticFiles(applicationBuilder); - + Assert.IsNotNull(result); Assert.IsTrue(result.Item1); Assert.IsFalse(result.Item2); Assert.IsFalse(result.Item3); - - var middlewareInstance = GetMiddlewareInstance(applicationBuilder)?.FirstOrDefault() as object?[]; + + var middlewareInstance = + GetMiddlewareInstance(applicationBuilder)?.FirstOrDefault() as object?[]; var value = middlewareInstance?.FirstOrDefault() as OptionsWrapper; - + Assert.IsFalse(value?.Value.RequestPath.HasValue); Assert.AreEqual(string.Empty, value?.Value.RequestPath.Value); } - + [TestMethod] public void BasicFlow_Assets() { var storage = new StorageHostFullPathFilesystem(); - storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, "wwwroot")); - storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, "clientapp", "build", "assets")); - + storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, + "wwwroot")); + storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, + "clientapp", "build", "assets")); + var startup = new Startup(); var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); @@ -154,19 +163,20 @@ public void BasicFlow_Assets() Assert.IsNotNull(result); Console.WriteLine("result:"); - Console.WriteLine("1: " +result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); - + Console.WriteLine("1: " + result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); + Assert.IsTrue(result.Item1); Assert.IsTrue(result.Item2); Assert.IsTrue(result.Item3); - var middlewareInstance = GetMiddlewareInstance(applicationBuilder)?.ToList()[1] as object?[]; + var middlewareInstance = + GetMiddlewareInstance(applicationBuilder)?.ToList()[1] as object?[]; var value = middlewareInstance?.FirstOrDefault() as OptionsWrapper; - + Assert.IsFalse(value?.Value.RequestPath.HasValue); Assert.AreEqual(string.Empty, value?.Value.RequestPath.Value); } - + [TestMethod] public void BasicFlow_Assets2() { @@ -175,31 +185,34 @@ public void BasicFlow_Assets2() serviceCollection.AddSingleton(); var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProviderInterface = serviceProvider.GetRequiredService(); - + var applicationBuilder = new ApplicationBuilder(serviceProviderInterface); startup.ConfigureServices(serviceCollection); - + var storage = new StorageHostFullPathFilesystem(); - storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, "wwwroot")); - storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, "clientapp", "build", "assets")); + storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, + "wwwroot")); + storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, + "clientapp", "build", "assets")); var result = startup.SetupStaticFiles(applicationBuilder); Assert.IsNotNull(result); - + Console.WriteLine("result:"); - Console.WriteLine("1: " +result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); - + Console.WriteLine("1: " + result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); + Assert.IsTrue(result.Item1); Assert.IsTrue(result.Item2); Assert.IsTrue(result.Item3); - var middlewareInstance = GetMiddlewareInstance(applicationBuilder)?.ToList()[2] as object?[]; + var middlewareInstance = + GetMiddlewareInstance(applicationBuilder)?.ToList()[2] as object?[]; var value = middlewareInstance?.FirstOrDefault() as OptionsWrapper; - + Assert.IsTrue(value?.Value.RequestPath.HasValue); Assert.AreEqual("/assets", value?.Value.RequestPath.Value); } - + [TestMethod] public void BasicFlow_Assets_NotFound() { @@ -208,19 +221,20 @@ public void BasicFlow_Assets_NotFound() serviceCollection.AddSingleton(); var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProviderInterface = serviceProvider.GetRequiredService(); - + var applicationBuilder = new ApplicationBuilder(serviceProviderInterface); startup.ConfigureServices(serviceCollection); - + var storage = new StorageHostFullPathFilesystem(); - storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, "wwwroot")); + storage.CreateDirectory(Path.Combine(new AppSettings().BaseDirectoryProject, + "wwwroot")); - var result = startup.SetupStaticFiles(applicationBuilder,"not-found-folder-name"); + var result = startup.SetupStaticFiles(applicationBuilder, "not-found-folder-name"); Assert.IsNotNull(result); - + Console.WriteLine("result:"); - Console.WriteLine("1: " +result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); - + Console.WriteLine("1: " + result.Item1 + " 2: " + result.Item2 + " 3: " + result.Item3); + Assert.IsTrue(result.Item1); Assert.IsTrue(result.Item2); Assert.IsFalse(result.Item3); @@ -236,7 +250,7 @@ public void PrepareResponse_CheckValues() Startup.PrepareResponse(context); // Assert Assert.IsNotNull(context.Context.Response.Headers.Expires); - Assert.AreEqual("public, max-age=31536000", + Assert.AreEqual("public, max-age=31536000", context.Context.Response.Headers.CacheControl.ToString()); } } diff --git a/starsky/starskytest/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModelTest.cs b/starsky/starskytest/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModelTest.cs new file mode 100644 index 0000000000..a613af43ac --- /dev/null +++ b/starsky/starskytest/starsky.feature.desktop/Models/PathImageFormatExistsAppPathModelTest.cs @@ -0,0 +1,40 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.feature.desktop.Models; +using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; + +namespace starskytest.starsky.feature.desktop.Models; + +[TestClass] +public class PathImageFormatExistsAppPathModelTest +{ + [TestMethod] + public void PathImageFormatExistsAppPathModelTest_Default() + { + var model = new PathImageFormatExistsAppPathModel(); + Assert.AreEqual(string.Empty, model.SubPath); + Assert.AreEqual(string.Empty, model.FullFilePath); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.notfound, model.ImageFormat); + Assert.AreEqual(FileIndexItem.ExifStatus.Default, model.Status); + Assert.AreEqual(string.Empty, model.AppPath); + } + + [TestMethod] + public void PathImageFormatExistsAppPathModelTest_Set() + { + var model = new PathImageFormatExistsAppPathModel + { + AppPath = "test", + Status = FileIndexItem.ExifStatus.Ok, + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg, + SubPath = "/test.jpg", + FullFilePath = "/test.jpg" + }; + + Assert.AreEqual("/test.jpg", model.SubPath); + Assert.AreEqual("/test.jpg", model.FullFilePath); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, model.ImageFormat); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, model.Status); + Assert.AreEqual("test", model.AppPath); + } +} diff --git a/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorDesktopServiceTest.cs b/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorDesktopServiceTest.cs new file mode 100644 index 0000000000..b262c5f3fc --- /dev/null +++ b/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorDesktopServiceTest.cs @@ -0,0 +1,366 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.feature.desktop.Models; +using starsky.feature.desktop.Service; +using starsky.foundation.database.Models; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeMocks; + +namespace starskytest.starsky.feature.desktop.Service; + +[TestClass] +public class OpenEditorDesktopServiceTest +{ + [TestMethod] + public async Task OpenAsync_stringInput_HappyFlow() + { + var fakeService = new FakeIOpenApplicationNativeService( + new List { "/test.jpg" }, "test"); + + var appSettings = new AppSettings + { + UseLocalDesktop = true, + DefaultDesktopEditor = new List + { + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "app", + ImageFormats = new List + { + ExtensionRolesHelper.ImageFormat.jpg + } + } + } + }; + + var preflight = new FakeIOpenEditorPreflight(new List + { + new PathImageFormatExistsAppPathModel + { + AppPath = "test", + Status = FileIndexItem.ExifStatus.Ok, + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg, + SubPath = "/test.jpg", + FullFilePath = "/test.jpg" + } + }); + + var service = + new OpenEditorDesktopService(appSettings, fakeService, preflight); + + var (success, status, list) = + await service.OpenAsync("/test.jpg;/test2.jpg", true); + + Assert.IsTrue(success); + Assert.AreEqual("Opened", status); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("/test.jpg", list[0].SubPath); + Assert.AreEqual("test", list[0].AppPath); + } + + + [TestMethod] + public async Task OpenAsync_ListInput_HappyFlow() + { + var fakeService = + new FakeIOpenApplicationNativeService(new List { "/test.jpg" }, "test"); + + var appSettings = new AppSettings + { + UseLocalDesktop = true, + DefaultDesktopEditor = new List + { + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "app", + ImageFormats = new List + { + ExtensionRolesHelper.ImageFormat.jpg + } + } + } + }; + + var preflight = new FakeIOpenEditorPreflight(new List + { + new PathImageFormatExistsAppPathModel + { + AppPath = "test", + Status = FileIndexItem.ExifStatus.Ok, + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg, + SubPath = "/test.jpg", + FullFilePath = "/test.jpg" + } + }); + + var service = + new OpenEditorDesktopService(appSettings, fakeService, preflight); + + var (success, status, list) = + await service.OpenAsync(new List { "/test.jpg" }, true); + + Assert.IsTrue(success); + Assert.AreEqual("Opened", status); + Assert.AreEqual(1, list.Count); + Assert.AreEqual("/test.jpg", list[0].SubPath); + Assert.AreEqual("test", list[0].AppPath); + } + + [TestMethod] + public async Task OpenAsync_ListInput_NoFilesSelected() + { + var fakeService = + new FakeIOpenApplicationNativeService(new List(), string.Empty); + + var appSettings = new AppSettings { UseLocalDesktop = true }; + + var preflight = new FakeIOpenEditorPreflight(new List()); + + var service = + new OpenEditorDesktopService(appSettings, fakeService, preflight); + + var (success, status, list) = + ( await service.OpenAsync(new List { "/test.jpg" }, true) ); + + Assert.IsFalse(success); + Assert.AreEqual("No files selected", status); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public async Task OpenAsync_ListInput_UseLocalDesktop_Null() + { + var fakeService = + new FakeIOpenApplicationNativeService(new List(), string.Empty); + + var appSettings = new AppSettings { UseLocalDesktop = false }; + + var preflight = new FakeIOpenEditorPreflight(new List()); + + var service = + new OpenEditorDesktopService(appSettings, fakeService, preflight); + + var (success, status, list) = + ( await service.OpenAsync(new List { "/test.jpg" }, true) ); + + Assert.IsNull(success); + Assert.AreEqual("UseLocalDesktop feature toggle is disabled", status); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public async Task OpenAsync_ListInput_UnSupportedPlatform() + { + var fakeService = new FakeIOpenApplicationNativeService(new List(), + string.Empty, false); + + var appSettings = new AppSettings { UseLocalDesktop = true }; + + var preflight = new FakeIOpenEditorPreflight(new List()); + + var service = new OpenEditorDesktopService(appSettings, fakeService, preflight); + + var (success, status, list) = + ( await service.OpenAsync(new List { "/test.jpg" }, true) ); + + Assert.IsNull(success); + Assert.AreEqual("OpenEditor is not supported on this configuration", status); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void OpenAmountConfirmationChecker_6Files() + { + var appSettings = new AppSettings { DesktopEditorAmountBeforeConfirmation = 5 }; + + var service = new OpenEditorDesktopService(appSettings, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + + var result = + service.OpenAmountConfirmationChecker( + "/test.jpg;/test2.jpg;/test3.jpg;/test4.jpg;/test5.jpg;/test6.jpg"); + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenAmountConfirmationChecker_6Files_Null() + { + var appSettings = new AppSettings { DesktopEditorAmountBeforeConfirmation = null }; + + var service = new OpenEditorDesktopService(appSettings, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + + var result = + service.OpenAmountConfirmationChecker( + "/test.jpg;/test2.jpg;/test3.jpg;/test4.jpg;/test5.jpg;/test6.jpg"); + + // Assumes that the default value is 5 + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenAmountConfirmationChecker_4Files() + { + var appSettings = new AppSettings { DesktopEditorAmountBeforeConfirmation = 4 }; + + var service = new OpenEditorDesktopService(appSettings, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + + var result = + service.OpenAmountConfirmationChecker("/test.jpg;/test2.jpg;/test3.jpg;/test4.jpg"); + Assert.IsTrue(result); + } + + [TestMethod] + public void OpenAmountConfirmationChecker_1File() + { + var appSettings = new AppSettings + { + DesktopEditorAmountBeforeConfirmation = -90 // invalid value + }; + + var service = new OpenEditorDesktopService(appSettings, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + + var result = service.OpenAmountConfirmationChecker("/test.jpg"); + Assert.IsTrue(result); + } + + [TestMethod] + public void IsEnabled_FalseDueFeatureFlag() + { + var appSettings = new AppSettings { UseLocalDesktop = false }; + var service = new OpenEditorDesktopService(appSettings, + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + var result = service.IsEnabled(); + Assert.IsFalse(result); + } + + [TestMethod] + public void IsEnabled_True() + { + var appSettings = new AppSettings + { + UseLocalDesktop = true // feature flag enabled + }; + var service = new OpenEditorDesktopService(appSettings, + // Default is supported in mock service + new FakeIOpenApplicationNativeService(new List(), "test"), + new FakeIOpenEditorPreflight(new List())); + var result = service.IsEnabled(); + Assert.IsTrue(result); + } + + [TestMethod] + public void IsEnabled_FalseDuePlatformNotSupported() + { + var appSettings = new AppSettings { UseLocalDesktop = true }; + var service = new OpenEditorDesktopService(appSettings, + // Is supported false! => + new FakeIOpenApplicationNativeService(new List(), "test", false), + new FakeIOpenEditorPreflight(new List())); + var result = service.IsEnabled(); + Assert.IsFalse(result); + } + + [TestMethod] + public void FilterListOpenDefaultEditorAndSpecificEditor_Test() + { + // Arrange + var inputList = new List + { + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file1.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = string.Empty + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file2.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = "editor.exe" + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file3.txt", + Status = FileIndexItem.ExifStatus.OperationNotSupported, + AppPath = string.Empty + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file4.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = string.Empty + } + }; + + // Act + var result = + OpenEditorDesktopService.FilterListOpenDefaultEditorAndSpecificEditor(inputList); + + // Assert + Assert.AreEqual(2, result.Item1.Count); // Expected number of files without AppPath + Assert.IsTrue( + result.Item1 + .Contains("file1.txt")); // Make sure file1.txt is in the list without AppPath + Assert.IsFalse( + result.Item1 + .Contains("file2.txt")); // Make sure file2.txt is not in the list without AppPath + Assert.AreEqual(1, result.Item2.Count); // Expected number of files with AppPath + Assert.IsTrue(result.Item2.Exists(x => + x.FullFilePath == "file2.txt" && + x.AppPath == + "editor.exe")); // Make sure file2.txt is in the list with AppPath and has correct editor + } + + [TestMethod] + public void FilterListOpenSpecificEditor_Test() + { + // Arrange + var inputList = new List + { + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file1.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = "" + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file2.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = "editor.exe" + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file3.txt", + Status = FileIndexItem.ExifStatus.NotFoundNotInIndex, + AppPath = "" + }, + new PathImageFormatExistsAppPathModel + { + FullFilePath = "file4.txt", + Status = FileIndexItem.ExifStatus.Ok, + AppPath = string.Empty + } + }; + + // Act + var result = + OpenEditorDesktopService.FilterListOpenDefaultEditorAndSpecificEditor(inputList); + + // Assert + Assert.AreEqual(1, result.Item2.Count); // Expected number of files with AppPath + Assert.IsTrue(result.Item2.Exists(x => + x is { FullFilePath: "file2.txt", AppPath: "editor.exe" })); + // Make sure file2.txt is in the list with AppPath and has correct editor + } +} diff --git a/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorPreflightTests.cs b/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorPreflightTests.cs new file mode 100644 index 0000000000..b5ec40f719 --- /dev/null +++ b/starsky/starskytest/starsky.feature.desktop/Service/OpenEditorPreflightTests.cs @@ -0,0 +1,557 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.feature.desktop.Service; +using starsky.foundation.database.Models; +using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeMocks; + +namespace starskytest.starsky.feature.desktop.Service; + +[TestClass] +public class OpenEditorPreflightTests +{ + [TestMethod] + public async Task PreflightAsync_NoAppPath() + { + // Arrange + var queryStub = new FakeIQuery(new List { new FileIndexItem("/test.jpg") }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(new List(), + new List { "/test.jpg" }); + + var inputFilePaths = new List { "/test.jpg" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + var result = await openEditorPreflight.PreflightAsync(inputFilePaths, collections); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.jpg", result[0].SubPath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.unknown, result[0].ImageFormat); + Assert.IsTrue(result[0].FullFilePath.EndsWith("test.jpg")); + Assert.AreEqual(string.Empty, result[0].AppPath); + } + + [TestMethod] + public async Task PreflightAsync_AppPathSet_ButNotFound() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.jpg") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + } + }); + var appSettingsStub = new AppSettings + { + DefaultDesktopEditor = new List + { + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "/app/test", + ImageFormats = new List + { + ExtensionRolesHelper.ImageFormat.jpg + } + } + } + }; + var storageStub = new FakeIStorage(new List(), + new List { "/test.jpg" }); + + var inputFilePaths = new List { "/test.jpg" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + var result = await openEditorPreflight.PreflightAsync(inputFilePaths, collections); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.jpg", result[0].SubPath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, result[0].ImageFormat); + Assert.IsTrue(result[0].FullFilePath.EndsWith("test.jpg")); + Assert.AreEqual(string.Empty, result[0].AppPath); + } + + [TestMethod] + public async Task PreflightAsync_AppPathSet() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.jpg") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + } + }); + var appSettingsStub = new AppSettings + { + DefaultDesktopEditor = new List + { + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "/app/test", + ImageFormats = new List + { + ExtensionRolesHelper.ImageFormat.jpg + } + } + } + }; + + // set a folder in the storage for app path location + var storageStub = new FakeIStorage(new List { "/app/test" }, + new List { "/test.jpg" }); + + var inputFilePaths = new List { "/test.jpg" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + var result = await openEditorPreflight.PreflightAsync(inputFilePaths, collections); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.jpg", result[0].SubPath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, result[0].ImageFormat); + Assert.IsTrue(result[0].FullFilePath.EndsWith("test.jpg")); + Assert.AreEqual("/app/test", result[0].AppPath); + } + + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_NotFound() + { + // Arrange + var queryStub = new FakeIQuery(new List { new FileIndexItem("/test.jpg") }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/test.jpg" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.jpg", result[0].FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, result[0].Status); + } + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_ReadOnly() + { + // Arrange + var queryStub = + new FakeIQuery(new List { new FileIndexItem("/readonly/test.jpg") }); + var appSettingsStub = new AppSettings + { + ReadOnlyFolders = new List { "/readonly" } + }; + var storageStub = + new FakeIStorage(new List(), + new List { "/readonly/test.jpg" }); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/readonly/test.jpg" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/readonly/test.jpg", result[0].FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.ReadOnly, result[0].Status); + } + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_SkipXmpSidecar() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.xmp") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.xmp + } + }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(new List(), + new List { "/test.xmp" }); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/test.xmp" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_ChangeDefaultToOkStatus() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.mp4") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.mp4, + Status = FileIndexItem.ExifStatus.Default // difference here! + } + }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(new List(), + new List { "/test.mp4" }); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/test.mp4" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + // Change the status to Ok + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.mp4", result[0].FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + } + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_Duplicates() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.mp4") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.mp4, + Status = FileIndexItem.ExifStatus.Ok + }, + new FileIndexItem("/test.mp4") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.mp4, + Status = FileIndexItem.ExifStatus.Ok // yes duplicates + } + }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(new List(), + new List { "/test.mp4" }); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/test.mp4" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + // removed duplicates + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.mp4", result[0].FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + } + + [TestMethod] + public async Task GetObjectsToOpenFromDatabase_ChangeOkAndSameToOkStatus() + { + // Arrange + var queryStub = new FakeIQuery(new List + { + new FileIndexItem("/test.mp4") + { + ImageFormat = ExtensionRolesHelper.ImageFormat.mp4, + Status = FileIndexItem.ExifStatus.OkAndSame // difference here! + } + }); + var appSettingsStub = new AppSettings(); + var storageStub = new FakeIStorage(new List(), + new List { "/test.mp4" }); + + // Assuming you have appropriate setup for your test case + var inputFilePaths = new List { "/test.mp4" }; + const bool collections = false; + + var openEditorPreflight = new OpenEditorPreflight(queryStub, appSettingsStub, + new FakeSelectorStorage(storageStub), new FakeIWebLogger()); + + // Act + var result = + await openEditorPreflight.GetObjectsToOpenFromDatabase(inputFilePaths, collections); + + // Change the status to Ok + Assert.AreEqual(1, result.Count); + Assert.AreEqual("/test.mp4", result[0].FilePath); + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[0].Status); + } + + [TestMethod] + public void GroupByFileCollectionName_ReturnsCorrectList_WhenAppSettingsIsDefault() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Default }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.jpg", + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + }, + new FileIndexItem + { + FileName = "collection1.tiff", + ImageFormat = ExtensionRolesHelper.ImageFormat.tiff + } + }; + + // Act + var result = preflight.GroupByFileCollectionName(fileIndexList); + + // Assert + Assert.AreEqual(1, result.Count); + + var collection1 = result.Find(p => p.FileCollectionName == "collection1"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, collection1?.ImageFormat); + } + + [TestMethod] + public void GroupByFileCollectionName_ReturnsCorrectList_WhenAppSettingsIsJpeg() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Jpeg }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.jpg", + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + }, + new FileIndexItem + { + FileName = "collection1.tiff", + ImageFormat = ExtensionRolesHelper.ImageFormat.tiff + }, + new FileIndexItem + { + FileName = "collection2.tiff", + ImageFormat = ExtensionRolesHelper.ImageFormat.tiff + }, + new FileIndexItem + { + FileName = "collection3.gif", + ImageFormat = ExtensionRolesHelper.ImageFormat.gif + } + }; + + // Act + var result = preflight.GroupByFileCollectionName(fileIndexList); + + // Assert + Assert.AreEqual(3, result.Count); + + var collection1 = result.Find(p => p.FileCollectionName == "collection1"); + var collection2 = result.Find(p => p.FileCollectionName == "collection2"); + var collection3 = result.Find(p => p.FileCollectionName == "collection3"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, collection1?.ImageFormat); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.tiff, collection2?.ImageFormat); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.gif, collection3?.ImageFormat); + } + + [TestMethod] + public void GroupByFileCollectionName_ReturnsCorrectList_WhenAppSettingsIsRaw() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.jpg", + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + }, + new FileIndexItem + { + FileName = "collection1.tiff", + ImageFormat = ExtensionRolesHelper.ImageFormat.tiff + } + }; + + // Act + var result = preflight.GroupByFileCollectionName(fileIndexList); + + // Assert + Assert.AreEqual(1, result.Count); + + var collection1 = result.Find(p => p.FileCollectionName == "collection1"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.tiff, collection1?.ImageFormat); + } + + [TestMethod] + public void GroupByFileCollectionName_ReturnsCorrectList_WhenAppSettingsIsOtherType() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.mp4", + ImageFormat = ExtensionRolesHelper.ImageFormat.mp4 + }, + new FileIndexItem + { + FileName = "collection1.jpg", + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + } + }; + + // Act + var result = preflight.GroupByFileCollectionName(fileIndexList); + + // Assert + Assert.AreEqual(1, result.Count); + + var collection1 = result.Find(p => p.FileCollectionName == "collection1"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.mp4, collection1?.ImageFormat); + } + + [TestMethod] + public void GroupByFileCollectionName_XmpFile_CollectionsFalse() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.xmp", + ImageFormat = ExtensionRolesHelper.ImageFormat.xmp + }, + new FileIndexItem + { + FileName = "collection1.jpg", + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + } + }; + + // Act + // Collection is disabled + var result = preflight.GroupByFileCollectionName(fileIndexList, false); + + // Assert + Assert.AreEqual(2, result.Count); + + var collection1Xmp = result.Find(p => p.FileName == "collection1.xmp"); + var collection1Jpg = result.Find(p => p.FileName == "collection1.jpg"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.xmp, collection1Xmp?.ImageFormat); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, collection1Jpg?.ImageFormat); + } + + [TestMethod] + public void GroupByFileCollectionName_Duplicates() + { + // Arrange + var query = new FakeIQuery(); // You can mock IQuery if needed + var appSettings = + new AppSettings { DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; + var iStorage = new FakeIStorage(); + var preflight = + new OpenEditorPreflight(query, appSettings, new FakeSelectorStorage(iStorage), + new FakeIWebLogger()); + + var fileIndexList = new List + { + new FileIndexItem + { + FileName = "collection1.jpg", // duplicate + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + }, + new FileIndexItem + { + FileName = "collection1.jpg", // duplicate + ImageFormat = ExtensionRolesHelper.ImageFormat.jpg + } + }; + + // Act + var result = preflight.GroupByFileCollectionName(fileIndexList); + + // Assert + Assert.AreEqual(1, result.Count); + + var collection1 = result.Find(p => p.FileCollectionName == "collection1"); + + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, collection1?.ImageFormat); + } +} diff --git a/starsky/starskytest/starsky.feature.geolookup/Services/GeoLocationWriteTest.cs b/starsky/starskytest/starsky.feature.geolookup/Services/GeoLocationWriteTest.cs index 000c4d49f4..74fdcd18c0 100644 --- a/starsky/starskytest/starsky.feature.geolookup/Services/GeoLocationWriteTest.cs +++ b/starsky/starskytest/starsky.feature.geolookup/Services/GeoLocationWriteTest.cs @@ -7,7 +7,6 @@ using starsky.foundation.writemeta.Interfaces; using starsky.foundation.writemeta.Services; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.geolookup.Services { @@ -21,7 +20,7 @@ public GeoLocationWriteTest() { // get the service _appSettings = new AppSettings(); - _exifTool = new FakeExifTool(new FakeIStorage(),_appSettings ); + _exifTool = new FakeExifTool(new FakeIStorage(), _appSettings); } [TestMethod] @@ -44,13 +43,14 @@ public async Task GeoLocationWriteLoopFolderTest() var console = new FakeConsoleWrapper(); var fakeIStorage = new FakeIStorage(); - await new GeoLocationWrite(_appSettings, _exifTool, - new FakeSelectorStorage(fakeIStorage),console, - new FakeIWebLogger(), new FakeIThumbnailQuery()).LoopFolderAsync(metaFilesInDirectory, true); + await new GeoLocationWrite(_appSettings, _exifTool, + new FakeSelectorStorage(fakeIStorage), console, + new FakeIWebLogger(), new FakeIThumbnailQuery()) + .LoopFolderAsync(metaFilesInDirectory, true); Assert.IsNotNull(metaFilesInDirectory); - - Assert.AreEqual(1,console.WrittenLines.Count); - Assert.AreEqual("🚀",console.WrittenLines[0]); + + Assert.AreEqual(1, console.WrittenLines.Count); + Assert.AreEqual("🚀", console.WrittenLines[0]); } [TestMethod] @@ -71,12 +71,13 @@ public async Task GeoLocationWriteLoopFolderTest_verbose() } }; var console = new FakeConsoleWrapper(); - await new GeoLocationWrite(new AppSettings{Verbose = true}, - _exifTool, new FakeSelectorStorage(),console, new FakeIWebLogger(), new FakeIThumbnailQuery()) - .LoopFolderAsync(metaFilesInDirectory, - true); + await new GeoLocationWrite(new AppSettings { Verbose = true }, + _exifTool, new FakeSelectorStorage(), console, new FakeIWebLogger(), + new FakeIThumbnailQuery()) + .LoopFolderAsync(metaFilesInDirectory, + true); - Assert.AreEqual(2,console.WrittenLines.Count); + Assert.AreEqual(2, console.WrittenLines.Count); Assert.IsTrue(console.WrittenLines.LastOrDefault()!.Contains("GeoLocationWrite")); } } diff --git a/starsky/starskytest/starsky.feature.import/Helpers/UpdateImportTransformationsTest.cs b/starsky/starskytest/starsky.feature.import/Helpers/UpdateImportTransformationsTest.cs index 8d891335db..f03356683d 100644 --- a/starsky/starskytest/starsky.feature.import/Helpers/UpdateImportTransformationsTest.cs +++ b/starsky/starskytest/starsky.feature.import/Helpers/UpdateImportTransformationsTest.cs @@ -8,7 +8,6 @@ using starsky.foundation.platform.Models; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.import.Helpers { diff --git a/starsky/starskytest/starsky.feature.import/Services/ImportTest.cs b/starsky/starskytest/starsky.feature.import/Services/ImportTest.cs index 2f5af38a2e..62fad6aad1 100644 --- a/starsky/starskytest/starsky.feature.import/Services/ImportTest.cs +++ b/starsky/starskytest/starsky.feature.import/Services/ImportTest.cs @@ -18,7 +18,6 @@ using starsky.foundation.storage.Services; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.import.Services { @@ -1155,7 +1154,7 @@ await importService.AddToQueryAndImportDatabaseAsync( Assert.AreEqual(0, logger.TrackedInformation.Count( p => p.Item2?.Contains("AddToQueryAndImportDatabaseAsync") == true)); } - + [TestMethod] public async Task RemoveFromQueryAndImportDatabaseAsync_NoConnection_NoVerbose() { diff --git a/starsky/starskytest/starsky.feature.import/Services/ImportTest_InMemoryDb.cs b/starsky/starskytest/starsky.feature.import/Services/ImportTest_InMemoryDb.cs index d76290180f..6265264a6f 100644 --- a/starsky/starskytest/starsky.feature.import/Services/ImportTest_InMemoryDb.cs +++ b/starsky/starskytest/starsky.feature.import/Services/ImportTest_InMemoryDb.cs @@ -17,7 +17,6 @@ using starsky.foundation.storage.Services; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.import.Services { @@ -39,158 +38,166 @@ public ImportTestInMemoryDb() var provider = new ServiceCollection() .AddMemoryCache(); - _appSettings = new AppSettings{ - DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase, - Verbose = true + _appSettings = new AppSettings + { + DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase, Verbose = true }; provider.AddSingleton(_appSettings); new SetupDatabaseTypes(_appSettings, provider).BuilderDb(); - provider.AddScoped(); + provider.AddScoped(); provider.AddScoped(); provider.AddScoped(); provider.AddSingleton(); var serviceProvider = provider.BuildServiceProvider(); - + _query = serviceProvider.GetRequiredService(); _importQuery = serviceProvider.GetRequiredService(); _console = new ConsoleWrapper(); - + _iStorageFake = new FakeIStorage( - new List{"/"}, - new List{"/test.jpg","/color_class_winner.jpg"}, - new List{CreateAnImage.Bytes.ToArray(), CreateAnImageColorClass.Bytes.ToArray()} + new List { "/" }, + new List { "/test.jpg", "/color_class_winner.jpg" }, + new List + { + CreateAnImage.Bytes.ToArray(), CreateAnImageColorClass.Bytes.ToArray() + } ); - + _exampleHash = new FileHash(_iStorageFake).GetHashCode("/test.jpg").Key; } - + [TestMethod] public async Task Importer_Gpx() { var storage = new FakeIStorage( - new List{"/"}, - new List{"/test.gpx"}, - new List{CreateAnGpx.Bytes.ToArray()}); - - var importService = new Import(new FakeSelectorStorage(storage), _appSettings, new FakeIImportQuery(), - new FakeExifTool(storage, _appSettings),_query,_console, new FakeIMetaExifThumbnailService(), new FakeIWebLogger(),new FakeIThumbnailQuery(), new FakeMemoryCache()); - var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(storage, _appSettings, "/test.gpx"); - - var result = await importService.Importer(new List {"/test.gpx"}, + new List { "/" }, + new List { "/test.gpx" }, + new List { CreateAnGpx.Bytes.ToArray() }); + + var importService = new Import(new FakeSelectorStorage(storage), _appSettings, + new FakeIImportQuery(), + new FakeExifTool(storage, _appSettings), _query, _console, + new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), + new FakeIThumbnailQuery(), new FakeMemoryCache()); + var expectedFilePath = + await ImportTest.GetExpectedFilePathAsync(storage, _appSettings, "/test.gpx"); + + var result = await importService.Importer(new List { "/test.gpx" }, new ImportSettingsModel()); var getResult = await _query.GetObjectByFilePathAsync(expectedFilePath); Assert.IsNotNull(getResult); - Assert.AreEqual(expectedFilePath,getResult.FilePath); + Assert.AreEqual(expectedFilePath, getResult.FilePath); Assert.AreEqual(ImportStatus.Ok, result[0].Status); - + await _query.RemoveItemAsync(getResult); } - + [TestMethod] public async Task Importer_OverwriteStructure_HappyFlow() { - var importService = new Import(new FakeSelectorStorage(_iStorageFake), + var importService = new Import(new FakeSelectorStorage(_iStorageFake), _appSettings, new FakeIImportQuery(), - new FakeExifTool(_iStorageFake, _appSettings),_query, _console, - new FakeIMetaExifThumbnailService(), new FakeIWebLogger(),new FakeIThumbnailQuery(),new FakeMemoryCache()); - - var result = await importService.Importer(new List {"/test.jpg"}, - new ImportSettingsModel{ + new FakeExifTool(_iStorageFake, _appSettings), _query, _console, + new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), + new FakeIThumbnailQuery(), new FakeMemoryCache()); + + var result = await importService.Importer(new List { "/test.jpg" }, + new ImportSettingsModel + { Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" }); - - var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, new AppSettings - { - Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" - }, "/test.jpg"); - - Assert.AreEqual(expectedFilePath,result[0].FilePath); + + var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, + new AppSettings { Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" }, + "/test.jpg"); + + Assert.AreEqual(expectedFilePath, result[0].FilePath); var queryResult = await _query.GetObjectByFilePathAsync(expectedFilePath); - + Assert.IsNotNull(queryResult); - Assert.AreEqual(expectedFilePath,queryResult.FilePath); + Assert.AreEqual(expectedFilePath, queryResult.FilePath); _iStorageFake.FileDelete(expectedFilePath); await _query.RemoveItemAsync(queryResult); } - + [TestMethod] public async Task Importer_HappyFlow_ItShouldAddTo_ImportDb() { - var importService = new Import(new FakeSelectorStorage(_iStorageFake), + var importService = new Import(new FakeSelectorStorage(_iStorageFake), _appSettings, _importQuery, - new FakeExifTool(_iStorageFake, _appSettings),_query, - _console, new FakeIMetaExifThumbnailService(), - new FakeIWebLogger(),new FakeIThumbnailQuery()); - - await importService.Importer(new List {"/test.jpg"}, - new ImportSettingsModel{ + new FakeExifTool(_iStorageFake, _appSettings), _query, + _console, new FakeIMetaExifThumbnailService(), + new FakeIWebLogger(), new FakeIThumbnailQuery()); + + await importService.Importer(new List { "/test.jpg" }, + new ImportSettingsModel + { Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" }); - + var isHashInImportDb = await _importQuery.IsHashInImportDbAsync(_exampleHash); Assert.IsTrue(isHashInImportDb); - - var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, new AppSettings - { - Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" - }, "/test.jpg"); - + + var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, + new AppSettings { Structure = "/yyyy/MM/yyyy_MM_dd*/_yyyyMMdd_HHmmss.ext" }, + "/test.jpg"); + var queryResult = await _query.GetObjectByFilePathAsync(expectedFilePath); Assert.IsNotNull(queryResult); _iStorageFake.FileDelete(expectedFilePath); await _query.RemoveItemAsync(queryResult); } - + [TestMethod] public async Task Importer_OverwriteColorClass() { - var importService = new Import(new FakeSelectorStorage(_iStorageFake), + var importService = new Import(new FakeSelectorStorage(_iStorageFake), _appSettings, new FakeIImportQuery(), new FakeExifTool(_iStorageFake, _appSettings), - _query, _console, new FakeIMetaExifThumbnailService(), + _query, _console, new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), new FakeIThumbnailQuery()); - var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, _appSettings, "/test.jpg"); - var result = await importService.Importer(new List {"/test.jpg"}, - new ImportSettingsModel{ - ColorClass = 5 - }); - + var expectedFilePath = + await ImportTest.GetExpectedFilePathAsync(_iStorageFake, _appSettings, "/test.jpg"); + var result = await importService.Importer(new List { "/test.jpg" }, + new ImportSettingsModel { ColorClass = 5 }); + Assert.IsNotNull(result.FirstOrDefault()); - Assert.AreEqual(expectedFilePath,result.FirstOrDefault()!.FilePath); + Assert.AreEqual(expectedFilePath, result.FirstOrDefault()!.FilePath); var queryResult = await _query.GetObjectByFilePathAsync(expectedFilePath); Assert.IsNotNull(queryResult); - Assert.AreEqual(expectedFilePath,queryResult.FilePath); - Assert.AreEqual(ColorClassParser.Color.Typical,queryResult.ColorClass); + Assert.AreEqual(expectedFilePath, queryResult.FilePath); + Assert.AreEqual(ColorClassParser.Color.Typical, queryResult.ColorClass); _iStorageFake.FileDelete(expectedFilePath); await _query.RemoveItemAsync(queryResult); } - + [TestMethod] public async Task Importer_ToDefaultFolderStructure_default_HappyFlow() { - var importService = new Import(new FakeSelectorStorage(_iStorageFake), + var importService = new Import(new FakeSelectorStorage(_iStorageFake), _appSettings, new FakeIImportQuery(), new FakeExifTool(_iStorageFake, _appSettings), - _query,_console, new FakeIMetaExifThumbnailService(), + _query, _console, new FakeIMetaExifThumbnailService(), new FakeIWebLogger(), new FakeIThumbnailQuery()); - var expectedFilePath = await ImportTest.GetExpectedFilePathAsync(_iStorageFake, _appSettings, "/test.jpg"); - var result = await importService.Importer(new List {"/test.jpg"}, + var expectedFilePath = + await ImportTest.GetExpectedFilePathAsync(_iStorageFake, _appSettings, "/test.jpg"); + var result = await importService.Importer(new List { "/test.jpg" }, new ImportSettingsModel()); - - Assert.AreEqual(expectedFilePath,result[0].FilePath); + + Assert.AreEqual(expectedFilePath, result[0].FilePath); var queryResult = await _query.GetObjectByFilePathAsync(expectedFilePath); Assert.IsNotNull(queryResult); - Assert.AreEqual(expectedFilePath,queryResult.FilePath); + Assert.AreEqual(expectedFilePath, queryResult.FilePath); _iStorageFake.FileDelete(expectedFilePath); await _query.RemoveItemAsync(queryResult); diff --git a/starsky/starskytest/starsky.feature.metaupdate/Services/DeleteItemTest.cs b/starsky/starskytest/starsky.feature.metaupdate/Services/DeleteItemTest.cs index 29d2b3997c..ac6309c193 100644 --- a/starsky/starskytest/starsky.feature.metaupdate/Services/DeleteItemTest.cs +++ b/starsky/starskytest/starsky.feature.metaupdate/Services/DeleteItemTest.cs @@ -18,7 +18,7 @@ public async Task Delete_FileNotFound_Ignore() var selectorStorage = new FakeSelectorStorage(new FakeIStorage()); var deleteItem = new DeleteItem(new FakeIQuery(), new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/not-found", true); - Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundNotInIndex, + Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundNotInIndex, result.FirstOrDefault()?.Status); } @@ -27,188 +27,210 @@ public async Task Delete_NotFoundOnDisk_Ignore() { var selectorStorage = new FakeSelectorStorage(new FakeIStorage()); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/exist-in-db.jpg")}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List { new FileIndexItem("/exist-in-db.jpg") }); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/exist-in-db.jpg", true); - Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, + Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, result.FirstOrDefault()?.Status); } - + [TestMethod] public async Task Delete_ReadOnly_Ignored() { - var selectorStorage = new FakeSelectorStorage(new FakeIStorage(new List{"/"}, - new List{"/readonly/test.jpg"}, new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()})); + var selectorStorage = new FakeSelectorStorage(new FakeIStorage(new List { "/" }, + new List { "/readonly/test.jpg" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() })); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/readonly/test.jpg")}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings{ReadOnlyFolders = new List{"/readonly"}}, selectorStorage); + new FakeIQuery(new List { new FileIndexItem("/readonly/test.jpg") }); + var deleteItem = new DeleteItem(fakeQuery, + new AppSettings { ReadOnlyFolders = new List { "/readonly" } }, + selectorStorage); var result = await deleteItem.DeleteAsync("/readonly/test.jpg", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.ReadOnly, + + Assert.AreEqual(FileIndexItem.ExifStatus.ReadOnly, result.FirstOrDefault()?.Status); } - + [TestMethod] public async Task Delete_StatusNotDeleted_Ignored() { - var selectorStorage = new FakeSelectorStorage(new FakeIStorage(new List{"/"}, - new List{"/test.jpg"}, new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()})); + var selectorStorage = new FakeSelectorStorage(new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() })); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test.jpg")}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List { new FileIndexItem("/test.jpg") }); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test.jpg", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, + + Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, result.FirstOrDefault()?.Status); } - + [TestMethod] public async Task Delete_IsFileRemoved() { - var storage = new FakeIStorage(new List {"/"}, - new List {"/test.jpg"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test.jpg") - {Tags = TrashKeyword.TrashKeywordString}}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test.jpg") { Tags = TrashKeyword.TrashKeywordString } + }); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test.jpg", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - + Assert.IsNull(fakeQuery.GetObjectByFilePath("/test.jpg")); Assert.IsFalse(storage.ExistFile("/test.jpg")); } - - + + [TestMethod] public async Task Delete_IsFileRemoved_WithCollection() { - var storage = new FakeIStorage(new List {"/", "/dir"}, - new List {"/dir/test.jpg"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/", "/dir" }, + new List { "/dir/test.jpg" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List { - new FileIndexItem("/dir") {IsDirectory = true, Tags = TrashKeyword.TrashKeywordString }, - - new FileIndexItem("/dir/test.jpg") {Tags = TrashKeyword.TrashKeywordString }, - new FileIndexItem("/dir/test.dng") {Tags = TrashKeyword.TrashKeywordString }} + new FakeIQuery(new List + { + new FileIndexItem("/dir") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/dir/test.jpg") + { + Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/dir/test.dng") + { + Tags = TrashKeyword.TrashKeywordString + } + } ); - - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/dir/test.jpg", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - + Assert.IsNull(fakeQuery.GetObjectByFilePath("/test.jpg")); Assert.IsFalse(storage.ExistFile("/test.jpg")); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result[1].Status); - + Assert.IsNull(fakeQuery.GetObjectByFilePath("/test.dng")); Assert.IsFalse(storage.ExistFile("/test.dng")); } - + [TestMethod] public async Task Delete_IsJsonSideCarFileRemoved() { - var storage = new FakeIStorage(new List {"/"}, - new List {"/test.jpg","/.starsky.test.jpg.json"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg", "/.starsky.test.jpg.json" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test.jpg") - {Tags = TrashKeyword.TrashKeywordString}}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test.jpg") { Tags = TrashKeyword.TrashKeywordString } + }); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test.jpg", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - + Assert.IsFalse(storage.ExistFile("/.starsky.test.jpg.json")); } - + [TestMethod] public async Task Delete_IsXmpSideCarFileRemoved() { - var storage = new FakeIStorage(new List {"/"}, - new List {"/test.dng","/test.xmp"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.dng", "/test.xmp" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test.dng") - {Tags = TrashKeyword.TrashKeywordString}}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test.dng") { Tags = TrashKeyword.TrashKeywordString } + }); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test.dng", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - + Assert.IsFalse(storage.ExistFile("/test.xmp")); } - + [TestMethod] public async Task Delete_IsFolderRemoved() { - var storage = new FakeIStorage(new List {"/test","/"}, - new List (), - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/test", "/" }, + new List(), + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test") - {IsDirectory = true, Tags = TrashKeyword.TrashKeywordString}}); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + } + }); + + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - + Assert.IsNull(fakeQuery.GetObjectByFilePath("/test")); Assert.IsFalse(storage.ExistFolder("/test")); } - + [TestMethod] public async Task Delete_IsFolderRemoved_IncludingChildFolders() { var storage = new FakeIStorage( - new List - { - "/test", - "/", - "/test/child_folder" - }, - new List {"/test/child_folder/i.jpg"}, - new List - { - FakeCreateAn.CreateAnImage.Bytes.ToArray() - }); + new List { "/test", "/", "/test/child_folder" }, + new List { "/test/child_folder/i.jpg" }, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); var fakeQuery = - new FakeIQuery(new List { - new FileIndexItem("/test"){IsDirectory = true, Tags = TrashKeyword.TrashKeywordString}, - new FileIndexItem("/test/child_folder"){IsDirectory = true}, - new FileIndexItem("/test/child_folder/2"){IsDirectory = true} + new FakeIQuery(new List + { + new FileIndexItem("/test") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/test/child_folder") { IsDirectory = true }, + new FileIndexItem("/test/child_folder/2") { IsDirectory = true } }); - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test", true); - - Assert.AreEqual(FileIndexItem.ExifStatus.Ok, + + Assert.AreEqual(FileIndexItem.ExifStatus.Ok, result.FirstOrDefault()?.Status); - - Assert.AreEqual(0,fakeQuery.GetAllFolders().Count); + + Assert.AreEqual(0, fakeQuery.GetAllFolders().Count); Assert.IsNull(fakeQuery.GetObjectByFilePath("/test")); Assert.IsNull(fakeQuery.GetObjectByFilePath("/test/child_folder")); Assert.IsNull(fakeQuery.GetObjectByFilePath("/test/child_folder/2")); @@ -218,18 +240,27 @@ public async Task Delete_IsFolderRemoved_IncludingChildFolders() [TestMethod] public async Task Delete_DirectoryWithChildItems_CollectionsOn() { - var storage = new FakeIStorage(new List {"/test","/"}, - new List {"/test/image.jpg", "/test/image.dng"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray(), - FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/test", "/" }, + new List { "/test/image.jpg", "/test/image.dng" }, + new List + { + FakeCreateAn.CreateAnImage.Bytes.ToArray(), + FakeCreateAn.CreateAnImage.Bytes.ToArray() + }); var selectorStorage = new FakeSelectorStorage(storage); - + var fakeQuery = - new FakeIQuery(new List {new FileIndexItem("/test") - {IsDirectory = true, Tags = TrashKeyword.TrashKeywordString}, new FileIndexItem("/test/image.jpg"), - new FileIndexItem("/test/image.dng")}); - - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/test/image.jpg"), + new FileIndexItem("/test/image.dng") + }); + + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test", true); Assert.AreEqual(3, result.Count); @@ -240,26 +271,31 @@ public async Task Delete_DirectoryWithChildItems_CollectionsOn() Assert.AreEqual(0, storage.GetAllFilesInDirectoryRecursive("/").Count()); Assert.AreEqual(0, fakeQuery.GetAllRecursive("/").Count); } - + [TestMethod] public async Task Delete_DirectoryWithChildItems_CollectionsOff() { - var storage = new FakeIStorage(new List {"/test","/"}, - new List {"/test/image.jpg", "/test/image.dng"}, - new List {FakeCreateAn.CreateAnImage.Bytes.ToArray(), - FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + var storage = new FakeIStorage(new List { "/test", "/" }, + new List { "/test/image.jpg", "/test/image.dng" }, + new List + { + FakeCreateAn.CreateAnImage.Bytes.ToArray(), + FakeCreateAn.CreateAnImage.Bytes.ToArray() + }); var selectorStorage = new FakeSelectorStorage(storage); - + var fakeQuery = - new FakeIQuery(new List { - new FileIndexItem("/test") { - IsDirectory = true, - Tags = TrashKeyword.TrashKeywordString - }, - new FileIndexItem("/test/image.jpg"), - new FileIndexItem("/test/image.dng")}); - - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + new FakeIQuery(new List + { + new FileIndexItem("/test") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/test/image.jpg"), + new FileIndexItem("/test/image.dng") + }); + + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test", false); Assert.AreEqual(3, result.Count); @@ -268,28 +304,33 @@ public async Task Delete_DirectoryWithChildItems_CollectionsOff() Assert.AreEqual("/test/image.dng", result[2].FilePath); Assert.AreEqual(0, storage.GetAllFilesInDirectoryRecursive("/").Count()); - Assert.AreEqual(0, (await fakeQuery.GetAllRecursiveAsync("/")).Count); + Assert.AreEqual(0, ( await fakeQuery.GetAllRecursiveAsync() ).Count); } [TestMethod] public async Task Delete_ChildDirectories() { - var storage = new FakeIStorage(new List {"/test", "/", "/test/child", "/test/child/child"}, - new List (), + var storage = new FakeIStorage( + new List { "/test", "/", "/test/child", "/test/child/child" }, + new List(), new List()); var selectorStorage = new FakeSelectorStorage(storage); - + var fakeQuery = - new FakeIQuery(new List { - new FileIndexItem("/test") {IsDirectory = true, Tags = TrashKeyword.TrashKeywordString}, - new FileIndexItem("/test/child") {IsDirectory = true}, - new FileIndexItem("/test/child/child") {IsDirectory = true}, + new FakeIQuery(new List + { + new FileIndexItem("/test") + { + IsDirectory = true, Tags = TrashKeyword.TrashKeywordString + }, + new FileIndexItem("/test/child") { IsDirectory = true }, + new FileIndexItem("/test/child/child") { IsDirectory = true }, }); - - var deleteItem = new DeleteItem( fakeQuery,new AppSettings(), selectorStorage); + + var deleteItem = new DeleteItem(fakeQuery, new AppSettings(), selectorStorage); var result = await deleteItem.DeleteAsync("/test", false); - + Assert.AreEqual(3, result.Count); Assert.AreEqual("/test", result[0].FilePath); Assert.AreEqual("/test/child", result[1].FilePath); diff --git a/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs b/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs index 309687595e..21f023d77d 100644 --- a/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs +++ b/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs @@ -17,7 +17,6 @@ using starsky.foundation.storage.Storage; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.metaupdate.Services { diff --git a/starsky/starskytest/starsky.feature.settings/Services/UpdateAppSettingsByPathTest.cs b/starsky/starskytest/starsky.feature.settings/Services/UpdateAppSettingsByPathTest.cs index daa6da93dd..395ac98b70 100644 --- a/starsky/starskytest/starsky.feature.settings/Services/UpdateAppSettingsByPathTest.cs +++ b/starsky/starskytest/starsky.feature.settings/Services/UpdateAppSettingsByPathTest.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.feature.settings.Services; +using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Helpers; using starsky.foundation.platform.JsonConverter; using starsky.foundation.platform.Models; using starsky.foundation.storage.Helpers; @@ -53,8 +55,8 @@ public async Task UpdateAppSettingsAsync_ValidInput_Success_CompareJson() var before = Environment.GetEnvironmentVariable("app__storageFolder"); Environment.SetEnvironmentVariable("app__storageFolder", string.Empty); - var testFolderPath = Path.DirectorySeparatorChar.ToString() + "test" + - Path.DirectorySeparatorChar.ToString(); + var testFolderPath = Path.DirectorySeparatorChar + "test" + + Path.DirectorySeparatorChar; var storage = new FakeIStorage(new List { "/", testFolderPath }); var selectorStorage = new FakeSelectorStorage(storage); @@ -62,7 +64,7 @@ public async Task UpdateAppSettingsAsync_ValidInput_Success_CompareJson() var updateAppSettingsByPath = new UpdateAppSettingsByPath(appSettings, selectorStorage); var appSettingTransferObject = new AppSettingsTransferObject { - StorageFolder = testFolderPath, Verbose = true, UseLocalDesktopUi = null + StorageFolder = testFolderPath, Verbose = true, }; // Act @@ -82,9 +84,10 @@ public async Task UpdateAppSettingsAsync_ValidInput_Success_CompareJson() // Assert var expectedResult = "{\n \"app\": {\n \"Verbose\": \"true\",\n \"StorageFolder\": " + // rm quotes - storageFolderJson + ",\n \"UseLocalDesktopUi\": \"false\"\n }\n}"; + storageFolderJson + ",\n"; - Assert.AreEqual(expectedResult, result); + + Assert.AreEqual(true, result.Contains(expectedResult)); } [TestMethod] @@ -191,5 +194,49 @@ await StreamToStringHelper.StreamToStringAsync( Assert.AreEqual(testFolderPath, fileResult2.App.StorageFolder); Assert.IsTrue(fileResult2.App.Verbose); } + + [TestMethod] + public async Task UpdateAppSettingsAsync_ValidInput_Success_Desktop() + { + var storage = new FakeIStorage(); + var selectorStorage = new FakeSelectorStorage(storage); + var updateAppSettingsByPath = + new UpdateAppSettingsByPath(new AppSettings(), selectorStorage); + var appSettingTransferObject = new AppSettingsTransferObject + { + DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw, + DefaultDesktopEditor = + [ + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "/test", + ImageFormats = + [ExtensionRolesHelper.ImageFormat.jpg] + } + ] + }; + + // Act + var result = + await updateAppSettingsByPath.UpdateAppSettingsAsync(appSettingTransferObject); + + + // Assert + Assert.AreEqual(200, result.StatusCode); + Assert.AreEqual("Updated", result.Message); + + var fileResultString2 = + await StreamToStringHelper.StreamToStringAsync( + storage.ReadStream(new AppSettings().AppSettingsPath)); + var fileResult2 = JsonSerializer.Deserialize(fileResultString2, + DefaultJsonSerializer.NoNamingPolicyBoolAsString); + + Assert.IsNotNull(fileResult2); + Assert.AreEqual(CollectionsOpenType.RawJpegMode.Raw, + fileResult2.App.DesktopCollectionsOpen); + Assert.AreEqual("/test", fileResult2.App.DefaultDesktopEditor[0].ApplicationPath); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.jpg, + fileResult2.App.DefaultDesktopEditor[0].ImageFormats[0]); + } } } diff --git a/starsky/starskytest/starsky.feature.trash/Services/MoveToTrashServiceTest.cs b/starsky/starskytest/starsky.feature.trash/Services/MoveToTrashServiceTest.cs index d78e5798ce..b0373989e7 100644 --- a/starsky/starskytest/starsky.feature.trash/Services/MoveToTrashServiceTest.cs +++ b/starsky/starskytest/starsky.feature.trash/Services/MoveToTrashServiceTest.cs @@ -16,7 +16,6 @@ using starsky.foundation.platform.Models; using starsky.foundation.readmeta.Services; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.trash.Services; @@ -29,23 +28,23 @@ public async Task InSystemTrash_ShouldMoveToTrash() const string path = "/test/test.jpg"; var trashService = new FakeITrashService(); var appSettings = new AppSettings { UseSystemTrash = true }; - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Ok - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, new FakeIMetaUpdateService(), + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Ok } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, new FakeIMetaUpdateService(), new FakeITrashConnectionService()); - await moveToTrashService.MoveToTrashAsync(new List{path}, true); - + await moveToTrashService.MoveToTrashAsync(new List { path }, true); + Assert.AreEqual(1, trashService.InTrash.Count); var expected = appSettings.StorageFolder + path.Replace('/', Path.DirectorySeparatorChar); Assert.AreEqual(expected, trashService.InTrash.FirstOrDefault()); } - + [TestMethod] public async Task InSystemTrash_ShouldMoveToTrash_Directory() { @@ -53,295 +52,286 @@ public async Task InSystemTrash_ShouldMoveToTrash_Directory() const string path = "/test/test.jpg"; var trashService = new FakeITrashService(); var appSettings = new AppSettings { UseSystemTrash = true }; - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{ + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List + { new FileIndexItem(path) { - IsDirectory = false, - Status = FileIndexItem.ExifStatus.Ok + IsDirectory = false, Status = FileIndexItem.ExifStatus.Ok }, new FileIndexItem(dirPath) { - IsDirectory = true, - Status = FileIndexItem.ExifStatus.Ok + IsDirectory = true, Status = FileIndexItem.ExifStatus.Ok } - }), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, new FakeIMetaUpdateService(), + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, new FakeIMetaUpdateService(), new FakeITrashConnectionService()); - await moveToTrashService.MoveToTrashAsync(new List{dirPath}, true); - + await moveToTrashService.MoveToTrashAsync(new List { dirPath }, true); + Assert.AreEqual(1, trashService.InTrash.Count); var expected = appSettings.StorageFolder + dirPath.Replace('/', Path.DirectorySeparatorChar); Assert.AreEqual(expected, trashService.InTrash.FirstOrDefault()); } - + [TestMethod] public async Task InSystemTrash_ShouldMoveToTrash_Status() { const string path = "/test/test.jpg"; var trashService = new FakeITrashService(); var appSettings = new AppSettings { UseSystemTrash = true }; - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Ok - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, new FakeIMetaUpdateService(), + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Ok } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, new FakeIMetaUpdateService(), new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); - - Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, result.FirstOrDefault()?.Status); + new List { path }, true); + + Assert.AreEqual(FileIndexItem.ExifStatus.NotFoundSourceMissing, + result.FirstOrDefault()?.Status); } - + [TestMethod] public async Task InMetaTrash_Status() { const string path = "/test/test.jpg"; var trashService = new FakeITrashService(); var appSettings = new AppSettings { UseSystemTrash = false }; - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Ok - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, new FakeIMetaUpdateService(), + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Ok } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, new FakeIMetaUpdateService(), new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); - + new List { path }, true); + Assert.AreEqual(FileIndexItem.ExifStatus.Deleted, result.FirstOrDefault()?.Status); } - + [TestMethod] public async Task InMetaTrash_StatusOk_IsNotSupported_AndEnabled() { const string path = "/test/test.jpg"; - var trashService = new FakeITrashService(){IsSupported = false}; + var trashService = new FakeITrashService() { IsSupported = false }; var appSettings = new AppSettings { UseSystemTrash = true }; // see supported var metaUpdate = new FakeIMetaUpdateService(); - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Ok - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, metaUpdate, + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Ok } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, metaUpdate, new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); - + new List { path }, true); + Assert.AreEqual(0, trashService.InTrash.Count); Assert.AreEqual(TrashKeyword.TrashKeywordString, result.FirstOrDefault()?.Tags); } - + [TestMethod] public async Task InMetaTrash_StatusOk_IsSupported_AndDisabled() { const string path = "/test/test.jpg"; - var trashService = new FakeITrashService(){IsSupported = true}; - var appSettings = new AppSettings { UseSystemTrash = false }; // see supported and other test + var trashService = new FakeITrashService() { IsSupported = true }; + var appSettings = + new AppSettings { UseSystemTrash = false }; // see supported and other test var metaUpdate = new FakeIMetaUpdateService(); - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Ok - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, metaUpdate, + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Ok } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, metaUpdate, new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); - + new List { path }, true); + Assert.AreEqual(0, trashService.InTrash.Count); Assert.AreEqual(TrashKeyword.TrashKeywordString, result.FirstOrDefault()?.Tags); } - + [TestMethod] public async Task InMetaTrash_StatusDeleted() { const string path = "/test/test.jpg"; - var trashService = new FakeITrashService(){IsSupported = false}; + var trashService = new FakeITrashService() { IsSupported = false }; var appSettings = new AppSettings { UseSystemTrash = true }; // see supported var metaUpdate = new FakeIMetaUpdateService(); - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Deleted - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, metaUpdate, + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Deleted } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, metaUpdate, new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); - + new List { path }, true); + Assert.AreEqual(0, trashService.InTrash.Count); Assert.AreEqual(TrashKeyword.TrashKeywordString, result.FirstOrDefault()?.Tags); } - + [TestMethod] public async Task InMetaTrash_WithDbContext() { const string path = "/test/test.jpg"; - - var trashService = new FakeITrashService(){IsSupported = false}; + + var trashService = new FakeITrashService() { IsSupported = false }; var appSettings = new AppSettings { - UseSystemTrash = false, - DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase + UseSystemTrash = false, DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase }; // see supported - + var builderDb = new DbContextOptionsBuilder(); builderDb.UseInMemoryDatabase(nameof(MoveToTrashServiceTest)); var options = builderDb.Options; var dbContext = new ApplicationDbContext(options); - var serviceCollection = new ServiceCollection().AddScoped(_ => new ApplicationDbContext(options)); - var serviceScopeFactory = serviceCollection.BuildServiceProvider().GetService(); - + var serviceCollection = + new ServiceCollection().AddScoped(_ => new ApplicationDbContext(options)); + var serviceScopeFactory = + serviceCollection.BuildServiceProvider().GetService(); + var storage = new FakeIStorage( - new List{"/", "/test"}, - new List{path} + new List { "/", "/test" }, + new List { path } ); var query = new Query(dbContext, appSettings, serviceScopeFactory, new FakeIWebLogger()); - var addedItem = await query.AddItemAsync(new FileIndexItem(path){Id = 9000}); - - var metaUpdate = new MetaUpdateService(query, new FakeExifTool(storage, appSettings), - new FakeSelectorStorage(storage), new MetaPreflight(query, appSettings, new FakeSelectorStorage(storage), - new FakeIWebLogger()), new FakeIWebLogger(), new ReadMetaSubPathStorage(new FakeSelectorStorage(storage), - appSettings, null!, new FakeIWebLogger()), new FakeIThumbnailService(), - new ThumbnailQuery(dbContext,null, new FakeIWebLogger())); + var addedItem = await query.AddItemAsync(new FileIndexItem(path) { Id = 9000 }); + + var metaUpdate = new MetaUpdateService(query, new FakeExifTool(storage, appSettings), + new FakeSelectorStorage(storage), new MetaPreflight(query, appSettings, + new FakeSelectorStorage(storage), + new FakeIWebLogger()), new FakeIWebLogger(), new ReadMetaSubPathStorage( + new FakeSelectorStorage(storage), + appSettings, null!, new FakeIWebLogger()), new FakeIThumbnailService(), + new ThumbnailQuery(dbContext, null, new FakeIWebLogger())); var metaPreflight = new MetaPreflight(query, appSettings, new FakeSelectorStorage(storage), new FakeIWebLogger()); - + var moveToTrashService = new MoveToTrashService(appSettings, query, metaPreflight, new FakeIUpdateBackgroundTaskQueue(), new TrashService(), metaUpdate, new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); + new List { path }, true); await query.RemoveItemAsync(addedItem); - + Assert.AreEqual(0, trashService.InTrash.Count); Assert.AreEqual(TrashKeyword.TrashKeywordString, result.FirstOrDefault()?.Tags); } - + [TestMethod] public async Task InMetaTrash_WithDbContext_Directory() { const string path = "/test"; const string childItem = "/test/test.jpg"; - - var trashService = new FakeITrashService(){IsSupported = false}; + + var trashService = new FakeITrashService() { IsSupported = false }; var appSettings = new AppSettings { - UseSystemTrash = false, - DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase + UseSystemTrash = false, DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase }; // see supported - + var builderDb = new DbContextOptionsBuilder(); builderDb.UseInMemoryDatabase(nameof(MoveToTrashServiceTest)); var options = builderDb.Options; var dbContext = new ApplicationDbContext(options); - var serviceCollection = new ServiceCollection().AddScoped(_ => new ApplicationDbContext(options)); - var serviceScopeFactory = serviceCollection.BuildServiceProvider().GetService(); - + var serviceCollection = + new ServiceCollection().AddScoped(_ => new ApplicationDbContext(options)); + var serviceScopeFactory = + serviceCollection.BuildServiceProvider().GetService(); + var storage = new FakeIStorage( - new List{"/", "/test"}, - new List{path} + new List { "/", "/test" }, + new List { path } ); var query = new Query(dbContext, appSettings, serviceScopeFactory, new FakeIWebLogger()); var addedItem = await query.AddRangeAsync(new List { - new FileIndexItem(path){Id = 8830, IsDirectory = true}, - new FileIndexItem(childItem){Id = 8831} + new FileIndexItem(path) { Id = 8830, IsDirectory = true }, + new FileIndexItem(childItem) { Id = 8831 } }); Console.WriteLine("add done"); - - var metaUpdate = new MetaUpdateService(query, new FakeExifTool(storage, appSettings), - new FakeSelectorStorage(storage), new MetaPreflight(query, appSettings, new FakeSelectorStorage(storage), - new FakeIWebLogger()), new FakeIWebLogger(), new ReadMetaSubPathStorage(new FakeSelectorStorage(storage), - appSettings, null!, new FakeIWebLogger()), new FakeIThumbnailService(), - new ThumbnailQuery(dbContext,null, new FakeIWebLogger())); + + var metaUpdate = new MetaUpdateService(query, new FakeExifTool(storage, appSettings), + new FakeSelectorStorage(storage), new MetaPreflight(query, appSettings, + new FakeSelectorStorage(storage), + new FakeIWebLogger()), new FakeIWebLogger(), new ReadMetaSubPathStorage( + new FakeSelectorStorage(storage), + appSettings, null!, new FakeIWebLogger()), new FakeIThumbnailService(), + new ThumbnailQuery(dbContext, null, new FakeIWebLogger())); var metaPreflight = new MetaPreflight(query, appSettings, new FakeSelectorStorage(storage), new FakeIWebLogger()); - + var moveToTrashService = new MoveToTrashService(appSettings, query, metaPreflight, new FakeIUpdateBackgroundTaskQueue(), new TrashService(), metaUpdate, new FakeITrashConnectionService()); var result = await moveToTrashService.MoveToTrashAsync( - new List{path}, true); + new List { path }, true); await query.RemoveItemAsync(addedItem); - + // not in system trash Assert.AreEqual(0, trashService.InTrash.Count); - + // result Assert.AreEqual(2, result.Count); Assert.AreEqual(TrashKeyword.TrashKeywordString, result[0].Tags); Assert.AreEqual(TrashKeyword.TrashKeywordString, result[1].Tags); } - - [TestMethod] - public void DetectToUseSystemTrash_False() - { - var trashService = new FakeITrashService(){IsSupported = false}; - var moveToTrashService = new MoveToTrashService(new AppSettings(), new FakeIQuery(), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, new FakeIMetaUpdateService(), - new FakeITrashConnectionService()); - - var result = moveToTrashService.DetectToUseSystemTrash(); - - Assert.AreEqual(false, result); - } [TestMethod] public async Task AppendChildItemsToTrashList_NoAny() { const string path = "/test/test.jpg"; - var trashService = new FakeITrashService(){IsSupported = false}; + var trashService = new FakeITrashService() { IsSupported = false }; var appSettings = new AppSettings { UseSystemTrash = true }; // see supported var metaUpdate = new FakeIMetaUpdateService(); - var moveToTrashService = new MoveToTrashService(appSettings, - new FakeIQuery(new List{new FileIndexItem(path) + var moveToTrashService = new MoveToTrashService(appSettings, + new FakeIQuery(new List { - Status = FileIndexItem.ExifStatus.Deleted - }}), - new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), - trashService, metaUpdate, + new FileIndexItem(path) { Status = FileIndexItem.ExifStatus.Deleted } + }), + new FakeMetaPreflight(), new FakeIUpdateBackgroundTaskQueue(), + trashService, metaUpdate, new FakeITrashConnectionService()); var (fileIndexResultsList, _) = await moveToTrashService.AppendChildItemsToTrashList( - new List - { - new FileIndexItem("") - }, new Dictionary>()); + new List { new FileIndexItem("") }, + new Dictionary>()); Assert.AreEqual(FileIndexItem.ExifStatus.Default, fileIndexResultsList.FirstOrDefault()?.Status); diff --git a/starsky/starskytest/starsky.feature.webhtmlpublish/Services/WebHtmlPublishServiceTest.cs b/starsky/starskytest/starsky.feature.webhtmlpublish/Services/WebHtmlPublishServiceTest.cs index 53f6bdd349..4b66ded3a5 100644 --- a/starsky/starskytest/starsky.feature.webhtmlpublish/Services/WebHtmlPublishServiceTest.cs +++ b/starsky/starskytest/starsky.feature.webhtmlpublish/Services/WebHtmlPublishServiceTest.cs @@ -11,7 +11,6 @@ using starsky.foundation.storage.Storage; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.feature.webhtmlpublish.Services { @@ -202,7 +201,8 @@ public async Task PreGenerateThumbnail_Test() new List { CreateAnImageNoExif.Bytes.ToArray() }); var selectorStorage = new FakeSelectorStorage(storage); - var service = new WebHtmlPublishService(new FakeIPublishPreflight(), selectorStorage, null!, + var service = new WebHtmlPublishService(new FakeIPublishPreflight(), selectorStorage, + null!, null!, null!, null!, new FakeIWebLogger(), new FakeIThumbnailService(selectorStorage)); var input = new List diff --git a/starsky/starskytest/Interfaces/IUserManagerTest.cs b/starsky/starskytest/starsky.foundation.accountmanagement/Interfaces/IUserManagerTest.cs similarity index 52% rename from starsky/starskytest/Interfaces/IUserManagerTest.cs rename to starsky/starskytest/starsky.foundation.accountmanagement/Interfaces/IUserManagerTest.cs index ed5e849444..8b9f6ab5ef 100644 --- a/starsky/starskytest/Interfaces/IUserManagerTest.cs +++ b/starsky/starskytest/starsky.foundation.accountmanagement/Interfaces/IUserManagerTest.cs @@ -2,7 +2,7 @@ using starsky.foundation.accountmanagement.Interfaces; using starsky.foundation.database.Models.Account; -namespace starskytest.Interfaces +namespace starskytest.starsky.foundation.accountmanagement.Interfaces { [TestClass] public sealed class UserManagerTest @@ -11,37 +11,38 @@ public sealed class UserManagerTest public void UserManagerTestSuccessFalse() { var error = new ChangeSecretResultError(); - var secretResult = new ChangeSecretResult(false,error); - Assert.AreEqual(false,secretResult.Success); + var secretResult = new ChangeSecretResult(false, error); + Assert.AreEqual(false, secretResult.Success); } - + [TestMethod] public void UserManagerTestChangeSecretResult() { var error = new ChangeSecretResultError(); - var secretResult = new ChangeSecretResult(false,error); - Assert.AreEqual(error,secretResult.Error); + var secretResult = new ChangeSecretResult(false, error); + Assert.AreEqual(error, secretResult.Error); } [TestMethod] public void UserManagerTestSignUpResult() { - var result = new SignUpResult(new User{Name = "test"}); - Assert.AreEqual("test",result.User?.Name); + var result = new SignUpResult(new User { Name = "test" }); + Assert.AreEqual("test", result.User?.Name); } - + [TestMethod] public void UserManagerTestSignUpResultFalse() { - var result = new SignUpResult(new User{Name = "test"},false, new SignUpResultError()); + var result = + new SignUpResult(new User { Name = "test" }, false, new SignUpResultError()); Assert.IsFalse(result.Success); } - + [TestMethod] public void UserManagerTestSignUpResultError() { - var result = new SignUpResult(null,false, new SignUpResultError()); - Assert.AreEqual(new SignUpResultError(),result.Error); + var result = new SignUpResult(null, false, new SignUpResultError()); + Assert.AreEqual(new SignUpResultError(), result.Error); } } } diff --git a/starsky/starskytest/starsky.foundation.accountmanagement/Services/UserManagerTest.cs b/starsky/starskytest/starsky.foundation.accountmanagement/Services/UserManagerTest.cs index 005d003b53..47f91797db 100644 --- a/starsky/starskytest/starsky.foundation.accountmanagement/Services/UserManagerTest.cs +++ b/starsky/starskytest/starsky.foundation.accountmanagement/Services/UserManagerTest.cs @@ -564,6 +564,44 @@ public void GetRole_NotExists() var result = userManager.GetRole("test12", "test"); Assert.IsNull(result); } + + [TestMethod] + public async Task GetRoleAsync_NotExists() + { + var userManager = new UserManager(_dbContext,new AppSettings(), new FakeIWebLogger(), _memoryCache); + var result = await userManager.GetRoleAsync(453454); + Assert.IsNull(result); + } + + [TestMethod] + public async Task GetRoleAsync_Exists() + { + _dbContext.Users.Add(new User{ Id = 45475, Name = "test"}); + _dbContext.Roles.Add(new Role { Code = "test_role_892453", Name = "test", Id = 47583945}); + _dbContext.UserRoles.Add(new UserRole{ UserId = 45475, RoleId = 47583945}); + + await _dbContext.SaveChangesAsync(); + var role = + await _dbContext.Roles.FirstOrDefaultAsync(p => p.Code == "test_role_892453"); + var userRole = + await _dbContext.UserRoles.FirstOrDefaultAsync(p => p.UserId == 45475); + var user= + await _dbContext.Users.FirstOrDefaultAsync(p => p.Id == 45475); + Assert.IsNotNull(role); + Assert.IsNotNull(userRole); + Assert.IsNotNull(user); + + var userManager = new UserManager(_dbContext,new AppSettings(), new FakeIWebLogger(), _memoryCache); + var result = await userManager.GetRoleAsync(45475); + + Assert.AreEqual("test_role_892453",result?.Code); + + _dbContext.Remove(role); + _dbContext.Remove(userRole); + _dbContext.Remove(user); + + await _dbContext.SaveChangesAsync(); + } [TestMethod] public async Task RemoveFromRole() diff --git a/starsky/starskytest/Extensions/EntityFrameworkExtensionsTest.cs b/starsky/starskytest/starsky.foundation.database/Extensions/EntityFrameworkExtensionsTest.cs similarity index 77% rename from starsky/starskytest/Extensions/EntityFrameworkExtensionsTest.cs rename to starsky/starskytest/starsky.foundation.database/Extensions/EntityFrameworkExtensionsTest.cs index 30fafc4e74..6483e7ebc6 100644 --- a/starsky/starskytest/Extensions/EntityFrameworkExtensionsTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Extensions/EntityFrameworkExtensionsTest.cs @@ -12,10 +12,10 @@ using starsky.foundation.database.Extensions; using starskytest.FakeMocks; -namespace starskytest.Extensions +namespace starskytest.starsky.foundation.database.Extensions { [TestClass] - public sealed class TestConnectionTest + public sealed class EntityFrameworkExtensionsTest { [TestMethod] public void TestConnection_Default() @@ -23,71 +23,71 @@ public void TestConnection_Default() var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; - + var context = new ApplicationDbContext(options); - Assert.AreEqual(true,context.TestConnection(new FakeIWebLogger())); + Assert.AreEqual(true, context.TestConnection(new FakeIWebLogger())); } - + [TestMethod] public void TestConnection_Mysql_Default() { var options = new DbContextOptionsBuilder() - .UseMySql("Server=localhost;Port=1234;database=test;uid=test;pwd=test;", - ServerVersion.Create(5, 0, 0,ServerType.MariaDb)) + .UseMySql("Server=localhost;Port=1234;database=test;uid=test;pwd=test;", + ServerVersion.Create(5, 0, 0, ServerType.MariaDb)) .Options; - + var context = new ApplicationDbContext(options); - Assert.AreEqual(true,context.TestConnection(new FakeIWebLogger())); + Assert.AreEqual(true, context.TestConnection(new FakeIWebLogger())); } - + [TestMethod] public void TestConnection_Cache_ShouldSetAfterWards() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; - + var context = new ApplicationDbContext(options); var provider = new ServiceCollection() .AddMemoryCache() .BuildServiceProvider(); var memoryCache = provider.GetRequiredService(); - + var result = context.TestConnection(new FakeIWebLogger(), memoryCache); - Assert.AreEqual(true,result); - + Assert.AreEqual(true, result); + memoryCache.TryGetValue("TestConnection", out var result2); Assert.AreEqual(true, result2); } - + [TestMethod] public void TestConnection_Cache_ShouldGetBefore() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; - + var context = new ApplicationDbContext(options); var provider = new ServiceCollection() .AddMemoryCache() .BuildServiceProvider(); var memoryCache = provider.GetRequiredService(); memoryCache.Set("TestConnection", false); - + var result = context.TestConnection(new FakeIWebLogger(), memoryCache); - Assert.AreEqual(false,result); - + Assert.AreEqual(false, result); + memoryCache.TryGetValue("TestConnection", out var result2); Assert.AreEqual(false, result2); } - - + + private class AppDbMySqlException : ApplicationDbContext { public AppDbMySqlException(DbContextOptions options) : base(options) { } - + private static MySqlException CreateMySqlException(string message) { // MySqlErrorCode errorCode, string? sqlState, string message, Exception? innerException @@ -96,38 +96,38 @@ private static MySqlException CreateMySqlException(string message) typeof(MySqlException).GetConstructors( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod); - var ctor = ctorLIst.FirstOrDefault(p => - p.ToString() == "Void .ctor(MySqlConnector.MySqlErrorCode, System.String, System.String, System.Exception)" ); - + var ctor = ctorLIst.FirstOrDefault(p => + p.ToString() == + "Void .ctor(MySqlConnector.MySqlErrorCode, System.String, System.String, System.Exception)"); + var instance = - ( MySqlException? ) ctor?.Invoke(new object[] + ( MySqlException? )ctor?.Invoke(new object[] { - MySqlErrorCode.AccessDenied, - "test", - message, - new Exception() + MySqlErrorCode.AccessDenied, "test", message, new Exception() }); return instance!; } - - public override DatabaseFacade Database => throw CreateMySqlException("Database is not available"); + + public override DatabaseFacade Database => + throw CreateMySqlException("Database is not available"); } - - + + [TestMethod] public void TestConnection_MySqlException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; - + var context = new AppDbMySqlException(options); var logger = new FakeIWebLogger(); var result = context.TestConnection(logger); - - Assert.AreEqual(false,result); - Assert.IsTrue(logger.TrackedInformation.FirstOrDefault().Item2?.Contains("Database is not available")); + + Assert.AreEqual(false, result); + Assert.IsTrue(logger.TrackedInformation.FirstOrDefault().Item2 + ?.Contains("Database is not available")); } - } + } } diff --git a/starsky/starskytest/Helpers/BreadcrumbHelperTest.cs b/starsky/starskytest/starsky.foundation.database/Helpers/BreadcrumbHelperTest.cs similarity index 94% rename from starsky/starskytest/Helpers/BreadcrumbHelperTest.cs rename to starsky/starskytest/starsky.foundation.database/Helpers/BreadcrumbHelperTest.cs index 29db2620bf..6bbbe7598a 100644 --- a/starsky/starskytest/Helpers/BreadcrumbHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Helpers/BreadcrumbHelperTest.cs @@ -2,11 +2,10 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Helpers; -using starskycore.Attributes; +using starsky.project.web.Attributes; -namespace starskytest.Helpers +namespace starskytest.starsky.foundation.database.Helpers { - /// /// Also known as BreadcrumbsTest /// @@ -22,7 +21,7 @@ public void BreadcrumbSlashMethodTest() var breadcrumblist = new List { "/" }; CollectionAssert.AreEqual(breadcrumbExample, breadcrumblist); } - + [TestMethod] public void BreadcrumbNoInputTest() { diff --git a/starsky/starskytest/starsky.foundation.database/Import/ImportQueryTest.cs b/starsky/starskytest/starsky.foundation.database/Import/ImportQueryTest.cs index 2ccac67318..d2c1d5f4da 100644 --- a/starsky/starskytest/starsky.foundation.database/Import/ImportQueryTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Import/ImportQueryTest.cs @@ -220,7 +220,7 @@ public async Task RemoveItemAsync_RemovesItemFromImportIndex() } [TestMethod] - public async Task RemoveAsync_Disposed() + public async Task ImportQuery_RemoveAsync_Disposed() { var addedItems = new List { diff --git a/starsky/starskytest/Models/Account/CredentialTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTest.cs similarity index 63% rename from starsky/starskytest/Models/Account/CredentialTest.cs rename to starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTest.cs index c18c8adb18..5e1e442967 100644 --- a/starsky/starskytest/Models/Account/CredentialTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTest.cs @@ -1,7 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models.Account; -namespace starskytest.Models.Account +namespace starskytest.starsky.foundation.database.Models.Account { [TestClass] public sealed class CredentialTest @@ -9,7 +9,6 @@ public sealed class CredentialTest [TestMethod] public void CredentialSetupTest() { - // public int Id // public int UserId // public int CredentialTypeId @@ -19,7 +18,7 @@ public void CredentialSetupTest() // public User User // public CredentialType CredentialType - var creds = new Credential + var credential = new Credential { Id = 0, UserId = 0, @@ -30,14 +29,13 @@ public void CredentialSetupTest() User = new User(), CredentialType = new CredentialType() }; - - Assert.AreEqual(0, creds.Id); - Assert.AreEqual(0, creds.UserId); - Assert.AreEqual(0, creds.CredentialTypeId); - Assert.AreEqual(string.Empty, creds.Identifier); - Assert.AreEqual( new User().Id, creds.User.Id); - Assert.AreEqual( new CredentialType().Code, creds.CredentialType.Code); + Assert.AreEqual(0, credential.Id); + Assert.AreEqual(0, credential.UserId); + Assert.AreEqual(0, credential.CredentialTypeId); + Assert.AreEqual(string.Empty, credential.Identifier); + Assert.AreEqual(new User().Id, credential.User.Id); + Assert.AreEqual(new CredentialType().Code, credential.CredentialType.Code); } } } diff --git a/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTypeTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTypeTest.cs index 2a6e136bd0..507452fefd 100644 --- a/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTypeTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/CredentialTypeTest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models.Account; @@ -10,9 +11,25 @@ public sealed class CredentialTypeTest public void CredentialType_Name_Credentials() { var rolePermission = new CredentialType(); - + Assert.IsNull(rolePermission.Name); Assert.IsNull(rolePermission.Credentials); } + + [TestMethod] + public void CredentialTypeSetup_Test() + { + var credentialType = new CredentialType + { + Id = 0, + Code = string.Empty, + Name = string.Empty, + Position = 0, + Credentials = new List() + }; + Assert.AreEqual(0, credentialType.Id); + Assert.AreEqual(0, credentialType.Position); + Assert.AreEqual(string.Empty, credentialType.Code); + } } } diff --git a/starsky/starskytest/starsky.foundation.database/Models/Account/PermissionTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/PermissionTest.cs new file mode 100644 index 0000000000..ba678de8f9 --- /dev/null +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/PermissionTest.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.database.Models.Account; + +namespace starskytest.starsky.foundation.database.Models.Account +{ + [TestClass] + public sealed class PermissionTest + { + [TestMethod] + public void CredentialSetupTest() + { + var permission = new Permission + { + Id = 0, Code = string.Empty, Name = string.Empty, Position = 0 + }; + Assert.AreEqual(0, permission.Id); + Assert.AreEqual(0, permission.Position); + Assert.AreEqual(string.Empty, permission.Code); + } + } +} diff --git a/starsky/starskytest/starsky.foundation.database/Models/Account/RolePermissionTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/RolePermissionTest.cs index 712b5e89c7..d858c45972 100644 --- a/starsky/starskytest/starsky.foundation.database/Models/Account/RolePermissionTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/RolePermissionTest.cs @@ -10,9 +10,21 @@ public sealed class RolePermissionTest public void RolePermission_Role_Permission() { var rolePermission = new RolePermission(); - + Assert.IsNull(rolePermission.Role); Assert.IsNull(rolePermission.Permission); } + + [TestMethod] + public void RolePermissionSetupTest() + { + // RoleId + PermissionId + var rolePermission = new RolePermission + { + RoleId = 0, PermissionId = 0, Role = new Role(), Permission = new Permission() + }; + Assert.AreEqual(0, rolePermission.RoleId); + Assert.AreEqual(0, rolePermission.PermissionId); + } } } diff --git a/starsky/starskytest/Models/Account/RoleTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/RoleTest.cs similarity index 66% rename from starsky/starskytest/Models/Account/RoleTest.cs rename to starsky/starskytest/starsky.foundation.database/Models/Account/RoleTest.cs index cc9c3884f4..c7efae7812 100644 --- a/starsky/starskytest/Models/Account/RoleTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/RoleTest.cs @@ -1,7 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models.Account; -namespace starskytest.Models.Account +namespace starskytest.starsky.foundation.database.Models.Account { [TestClass] public sealed class RoleTest @@ -9,14 +9,7 @@ public sealed class RoleTest [TestMethod] public void RoleSetupTest() { - - var role = new Role - { - Id = 0, - Code = string.Empty, - Name = string.Empty, - Position = 0 - }; + var role = new Role { Id = 0, Code = string.Empty, Name = string.Empty, Position = 0 }; Assert.AreEqual(0, role.Id); Assert.AreEqual(0, role.Position); Assert.AreEqual(string.Empty, role.Code); diff --git a/starsky/starskytest/starsky.foundation.database/Models/Account/UserRoleTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/UserRoleTest.cs index ec94c9ae2a..7eae001f77 100644 --- a/starsky/starskytest/starsky.foundation.database/Models/Account/UserRoleTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/UserRoleTest.cs @@ -10,9 +10,20 @@ public sealed class UserRoleTest public void UserRole_User_Role() { var rolePermission = new UserRole(); - + Assert.IsNull(rolePermission.Role); Assert.IsNull(rolePermission.User); } + + [TestMethod] + public void UserRoleTest_SetupTest() + { + var role = new UserRole() + { + UserId = 0, RoleId = 0, User = new User(), Role = new Role() + }; + Assert.AreEqual(0, role.UserId); + Assert.AreEqual(0, role.RoleId); + } } } diff --git a/starsky/starskytest/Models/Account/UserTest.cs b/starsky/starskytest/starsky.foundation.database/Models/Account/UserTest.cs similarity index 89% rename from starsky/starskytest/Models/Account/UserTest.cs rename to starsky/starskytest/starsky.foundation.database/Models/Account/UserTest.cs index 7ff1450d61..0674a1e61e 100644 --- a/starsky/starskytest/Models/Account/UserTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/Account/UserTest.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models.Account; -namespace starskytest.Models.Account +namespace starskytest.starsky.foundation.database.Models.Account { [TestClass] public sealed class UserTest diff --git a/starsky/starskytest/starsky.foundation.database/Models/FileIndexItemTest.cs b/starsky/starskytest/starsky.foundation.database/Models/FileIndexItemTest.cs index d9d0a8f417..04eb04a079 100644 --- a/starsky/starskytest/starsky.foundation.database/Models/FileIndexItemTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Models/FileIndexItemTest.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; +using starsky.foundation.writemeta.Helpers; namespace starskytest.starsky.foundation.database.Models { @@ -14,135 +15,141 @@ public sealed class FileIndexItemTest [TestMethod] public void FileIndexItemTest_SetTagsToNull() { - var item = new FileIndexItem{Tags = null}; - Assert.AreEqual(item.Tags,string.Empty); + var item = new FileIndexItem { Tags = null }; + Assert.AreEqual(item.Tags, string.Empty); } - + [TestMethod] public void FileIndexItemTest_KeywordsToNull() { - var item = new FileIndexItem{Keywords = null}; + var item = new FileIndexItem { Keywords = null }; // > read tags instead of keywords - Assert.AreEqual(item.Tags,string.Empty); + Assert.AreEqual(item.Tags, string.Empty); } [TestMethod] public void FileIndexItem_DoubleSpaces() { - var item = new FileIndexItem{Tags = "test0, test1  ,   test2,   test3, " + - "test4,   test5, test6, test7,   "}; - + var item = new FileIndexItem + { + Tags = "test0, test1  ,   test2,   test3, " + + "test4,   test5, test6, test7,   " + }; + Assert.AreEqual("test1", item.Keywords?.ToList()[1]); Assert.AreEqual("test2", item.Keywords?.ToList()[2]); Assert.AreEqual("test5", item.Keywords?.ToList()[5]); Assert.AreEqual("test7", item.Keywords?.ToList()[7]); - Assert.AreEqual(8,item.Keywords?.Count); + Assert.AreEqual(8, item.Keywords?.Count); } - + [TestMethod] public void FileIndexItemTest_SetDescriptionsToNull() { - var item = new FileIndexItem{Description = null}; - Assert.AreEqual(item.Description,string.Empty); + var item = new FileIndexItem { Description = null }; + Assert.AreEqual(item.Description, string.Empty); } - - + + [TestMethod] public void FileIndexItemTest_SetColorClassTestDefault() { var input = ColorClassParser.GetColorClass(); var output = ColorClassParser.Color.None; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTestMin1() { var input = ColorClassParser.GetColorClass("-1"); var output = ColorClassParser.Color.DoNotChange; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] public void FileIndexItemTest_SetColorClassTest0() { var input = ColorClassParser.GetColorClass("0"); var output = ColorClassParser.Color.None; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest1() { var input = ColorClassParser.GetColorClass("1"); var output = ColorClassParser.Color.Winner; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest2() { var input = ColorClassParser.GetColorClass("2"); var output = ColorClassParser.Color.WinnerAlt; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest3() { var input = ColorClassParser.GetColorClass("3"); var output = ColorClassParser.Color.Superior; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest4() { var input = ColorClassParser.GetColorClass("4"); var output = ColorClassParser.Color.SuperiorAlt; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest5() { var input = ColorClassParser.GetColorClass("5"); var output = ColorClassParser.Color.Typical; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest6() { var input = ColorClassParser.GetColorClass("6"); var output = ColorClassParser.Color.TypicalAlt; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest7() { var input = ColorClassParser.GetColorClass("7"); var output = ColorClassParser.Color.Extras; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } - + [TestMethod] public void FileIndexItemTest_SetColorClassTest8() { var input = ColorClassParser.GetColorClass("8"); var output = ColorClassParser.Color.Trash; - Assert.AreEqual(input,output); + Assert.AreEqual(input, output); } [TestMethod] public void FileIndexItemTest_GetColorClassListTestEightSeven() { var input = "8,7"; - var eightSeven = new List {ColorClassParser.Color.Trash,ColorClassParser.Color.Extras}; + var eightSeven = new List + { + ColorClassParser.Color.Trash, ColorClassParser.Color.Extras + }; var output = FileIndexItem.GetColorClassList(input); - CollectionAssert.AreEqual(eightSeven,output); + CollectionAssert.AreEqual(eightSeven, output); } [TestMethod] @@ -150,80 +157,80 @@ public void FileIndexItemTest_GetColorClassListString() { var input = "string"; var output = FileIndexItem.GetColorClassList(input); - Assert.AreEqual(0,output.Count); // <= 0 + Assert.AreEqual(0, output.Count); // <= 0 } [TestMethod] public void FileIndexItemTest_FileIndexItemTitleTest() { - var fileIndexItem = new FileIndexItem {Title = null}; - Assert.AreEqual(fileIndexItem.Title,string.Empty); + var fileIndexItem = new FileIndexItem { Title = null }; + Assert.AreEqual(fileIndexItem.Title, string.Empty); } [TestMethod] public void FileIndexItemTest_FileNameNull() { var t = new FileIndexItem(); - Assert.AreEqual(string.Empty,t.FileName); + Assert.AreEqual(string.Empty, t.FileName); } - + [TestMethod] public void FileIndexItemTest_ParentDirectoryNull() { var t = new FileIndexItem(); - Assert.AreEqual(string.Empty,t.ParentDirectory); + Assert.AreEqual(string.Empty, t.ParentDirectory); } - + [TestMethod] public void FileIndexItemTest_FilePathNull() { var t = new FileIndexItem(); - Assert.AreEqual("/",t.FilePath); + Assert.AreEqual("/", t.FilePath); } - + [TestMethod] public void FileIndexItemTest_OrientationrelativeRotation0() { // keep the same - var t = new FileIndexItem {Orientation = FileIndexItem.Rotation.Horizontal}; - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,t.RelativeOrientation()); + var t = new FileIndexItem { Orientation = FileIndexItem.Rotation.Horizontal }; + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, t.RelativeOrientation()); } [TestMethod] public void FileIndexItemTest_SetOrientationrelativeRotation0() { - var fileObject = new FileIndexItem {Orientation = FileIndexItem.Rotation.Horizontal}; + var fileObject = new FileIndexItem { Orientation = FileIndexItem.Rotation.Horizontal }; fileObject.SetRelativeOrientation(); - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,fileObject.Orientation); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, fileObject.Orientation); } - + [TestMethod] public void FileIndexItemTest_OrientationrelativeRotation_270CwTest() { - var fileObject = new FileIndexItem {Orientation = FileIndexItem.Rotation.Rotate270Cw}; - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,fileObject.RelativeOrientation(1)); + var fileObject = new FileIndexItem { Orientation = FileIndexItem.Rotation.Rotate270Cw }; + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, fileObject.RelativeOrientation(1)); } - + [TestMethod] public void FileIndexItemTest_SetOrientationrelativeRotationMinus1() { - var t = new FileIndexItem {Orientation = FileIndexItem.Rotation.Horizontal}; - Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw,t.RelativeOrientation(-1)); + var t = new FileIndexItem { Orientation = FileIndexItem.Rotation.Horizontal }; + Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw, t.RelativeOrientation(-1)); } [TestMethod] public void FileIndexItemTest_SetOrientationrelativeRotationPlus1() { - var t = new FileIndexItem {Orientation = FileIndexItem.Rotation.Horizontal}; - Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw,t.RelativeOrientation(1)); + var t = new FileIndexItem { Orientation = FileIndexItem.Rotation.Horizontal }; + Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw, t.RelativeOrientation(1)); } - + [TestMethod] public void FileIndexItemTest_SetOrientationrelativeRotation_Rotate270Cw_Plus1() { - var t = new FileIndexItem {Orientation = FileIndexItem.Rotation.Rotate270Cw}; - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,t.RelativeOrientation(1)); + var t = new FileIndexItem { Orientation = FileIndexItem.Rotation.Rotate270Cw }; + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, t.RelativeOrientation(1)); } @@ -231,93 +238,97 @@ public void FileIndexItemTest_SetOrientationrelativeRotation_Rotate270Cw_Plus1() public void FileIndexItemTest_SetOrientationrelativeRelativeOrientation_Plus5() { // test not very good - var t = new FileIndexItem {Orientation = FileIndexItem.Rotation.Rotate270Cw}; - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,t.RelativeOrientation(5)); + var t = new FileIndexItem { Orientation = FileIndexItem.Rotation.Rotate270Cw }; + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, t.RelativeOrientation(5)); } [TestMethod] public void FileIndexItemTest_SetAbsoluteOrientation_DoNotChange() { var rotationItem = new FileIndexItem().SetAbsoluteOrientation(); - Assert.AreEqual(FileIndexItem.Rotation.DoNotChange,rotationItem); + Assert.AreEqual(FileIndexItem.Rotation.DoNotChange, rotationItem); } [TestMethod] public void FileIndexItemTest_SetAbsoluteOrientation_Rotate90Cw() { var rotationItem = new FileIndexItem().SetAbsoluteOrientation("6"); - Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw,rotationItem); + Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw, rotationItem); } [TestMethod] public void FileIndexItemTest_SetAbsoluteOrientation_Rotate180() { var rotationItem = new FileIndexItem().SetAbsoluteOrientation("3"); - Assert.AreEqual(FileIndexItem.Rotation.Rotate180,rotationItem); + Assert.AreEqual(FileIndexItem.Rotation.Rotate180, rotationItem); } - + [TestMethod] public void FileIndexItemTest_SetAbsoluteOrientation_Rotate270Cw() { var rotationItem = new FileIndexItem().SetAbsoluteOrientation("8"); - Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw,rotationItem); + Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw, rotationItem); } [TestMethod] public void FileIndexItemTest_colorDisplayName_WinnerAlt() { var colorDisplayName = EnumHelper.GetDisplayName(ColorClassParser.Color.WinnerAlt); - Assert.AreEqual("Winner Alt",colorDisplayName); + Assert.AreEqual("Winner Alt", colorDisplayName); } - + [TestMethod] public void FileIndexItemTest_MakeModel_UsingField() { - var item = new FileIndexItem{MakeModel = "Apple|iPhone SE|iPhone SE back camera 4.15mm f/2.2"}; + var item = new FileIndexItem + { + MakeModel = "Apple|iPhone SE|iPhone SE back camera 4.15mm f/2.2" + }; Assert.AreEqual("Apple", item.Make); - Assert.AreEqual("iPhone SE",item.Model); - Assert.AreEqual("back camera 4.15mm f/2.2",item.LensModel); + Assert.AreEqual("iPhone SE", item.Model); + Assert.AreEqual("back camera 4.15mm f/2.2", item.LensModel); } [TestMethod] public void LensModel_Defaults() { - var item = new FileIndexItem{MakeModel = string.Empty}; - Assert.AreEqual(string.Empty,item.LensModel); + var item = new FileIndexItem { MakeModel = string.Empty }; + Assert.AreEqual(string.Empty, item.LensModel); } + [TestMethod] public void LensModel_ShouldReplace() { - var item = new FileIndexItem{MakeModel = "test|Canon|Canon Lens"}; - Assert.AreEqual("Lens",item.LensModel); + var item = new FileIndexItem { MakeModel = "test|Canon|Canon Lens" }; + Assert.AreEqual("Lens", item.LensModel); } - + [TestMethod] public void LensModel_ShouldNotReplace() { - var item = new FileIndexItem{MakeModel = "test||Canon Lens"}; - Assert.AreEqual("Canon Lens",item.LensModel); + var item = new FileIndexItem { MakeModel = "test||Canon Lens" }; + Assert.AreEqual("Canon Lens", item.LensModel); } - + [TestMethod] public void FileIndexItemTest_MakeModel_UsingFieldNull() { - var item = new FileIndexItem{MakeModel = null}; + var item = new FileIndexItem { MakeModel = null }; Assert.AreEqual(string.Empty, item.Make); } - + [TestMethod] public void FileIndexItemTest_MakeModel_UsingFieldNullLensModel() { - var item = new FileIndexItem{MakeModel = null}; + var item = new FileIndexItem { MakeModel = null }; Assert.AreEqual(string.Empty, item.LensModel); } - + [TestMethod] public void FileIndexItemTest_MakeModel_IgnoreDashDash() { - var item = new FileIndexItem{MakeModel = null}; - item.SetMakeModel("----",0); + var item = new FileIndexItem { MakeModel = null }; + item.SetMakeModel("----", 0); Assert.AreEqual(string.Empty, item.LensModel); } @@ -344,7 +355,7 @@ public void FileIndexItemTest_SetMakeModel_Make() [TestMethod] public void FileIndexItemTest_SetMakeModel_MakeWrongPipeLength() { - var item = new FileIndexItem{MakeModel = "Apple|||||||"}; + var item = new FileIndexItem { MakeModel = "Apple|||||||" }; Assert.AreEqual(string.Empty, item.Make); Assert.AreEqual(string.Empty, item.Model); } @@ -394,13 +405,13 @@ public void FileIndexItemTest_SetMakeModel_RightOrder_MakeANDModel() public void FileIndexItemTest_IsRelativeOrientation() { var item = FileIndexItem.IsRelativeOrientation(-1); - Assert.AreEqual(true,item); - + Assert.AreEqual(true, item); + var item2 = FileIndexItem.IsRelativeOrientation(1); - Assert.AreEqual(true,item2); - + Assert.AreEqual(true, item2); + var item999 = FileIndexItem.IsRelativeOrientation(999); - Assert.AreEqual(false,item999); + Assert.AreEqual(false, item999); } @@ -408,42 +419,42 @@ public void FileIndexItemTest_IsRelativeOrientation() public void FileIndexItemTest_Ctor_SpaceName() { var item = new FileIndexItem("/test/image with space.jpg"); - Assert.AreEqual("image with space.jpg",item.FileName); - Assert.AreEqual("image with space",item.FileCollectionName); - Assert.AreEqual("/test",item.ParentDirectory); + Assert.AreEqual("image with space.jpg", item.FileName); + Assert.AreEqual("image with space", item.FileCollectionName); + Assert.AreEqual("/test", item.ParentDirectory); } [TestMethod] public void SidecarExtensions_read() { - var item = new FileIndexItem{SidecarExtensions = "xmp|test"}; + var item = new FileIndexItem { SidecarExtensions = "xmp|test" }; Assert.AreEqual("xmp", item.SidecarExtensionsList.FirstOrDefault()); } - + [TestMethod] public void SidecarExtensions_read_null() { - var item = new FileIndexItem{SidecarExtensions = null}; + var item = new FileIndexItem { SidecarExtensions = null }; Assert.AreEqual(0, item.SidecarExtensionsList.Count); } - + [TestMethod] public void SidecarExtensions_Add() { - var item = new FileIndexItem{SidecarExtensions = "xmp"}; + var item = new FileIndexItem { SidecarExtensions = "xmp" }; item.AddSidecarExtension("xmp"); - + Assert.AreEqual("xmp", item.SidecarExtensionsList.FirstOrDefault()); // no duplicates please Assert.AreEqual(1, item.SidecarExtensionsList.Count); } - + [TestMethod] public void SidecarExtensions_Remove() { - var item = new FileIndexItem{SidecarExtensions = "xmp"}; + var item = new FileIndexItem { SidecarExtensions = "xmp" }; item.RemoveSidecarExtension("xmp"); - + Assert.AreEqual(0, item.SidecarExtensionsList.Count); } @@ -452,69 +463,69 @@ public void SetFilePath_Home() { var item = new FileIndexItem(); item.SetFilePath("/"); - + Assert.AreEqual("/", item.FileName); Assert.AreEqual(string.Empty, item.ParentDirectory); } - + [TestMethod] public void SetFilePath_testFile() { var item = new FileIndexItem(); item.SetFilePath("/test.jpg"); - + Assert.AreEqual("test.jpg", item.FileName); Assert.AreEqual("/", item.ParentDirectory); } - + [TestMethod] public void SetFilePath_slashSlashTestFile() { var item = new FileIndexItem(); item.SetFilePath("//test.jpg"); - + Assert.AreEqual("test.jpg", item.FileName); Assert.AreEqual("/", item.ParentDirectory); Assert.AreEqual("/test.jpg", item.FilePath); } - + [TestMethod] public void SetFilePath_subFolderTestFile() { var item = new FileIndexItem(); item.SetFilePath("/test/test.jpg"); - + Assert.AreEqual("test.jpg", item.FileName); Assert.AreEqual("/test", item.ParentDirectory); } - - + + [TestMethod] public void Size_Lt_0() { var value = -1; - var item = new FileIndexItem(){Size = value}; - + var item = new FileIndexItem() { Size = value }; + Assert.AreEqual(0, item.Size); } - + [TestMethod] public void Size_MinValue() { - var item = new FileIndexItem(){Size = 99999999999999999}; + var item = new FileIndexItem() { Size = 99999999999999999 }; // overwrite here, should not be 0 item.Size = int.MinValue; - + // should write to large values to min value Assert.AreEqual(0, item.Size); } - + [TestMethod] public void Size_ShouldAdd() { var value = 2; - var item = new FileIndexItem(){Size = value}; - + var item = new FileIndexItem() { Size = value }; + Assert.AreEqual(2, item.Size); } @@ -523,17 +534,18 @@ public void FixedListToString_Null() { Assert.AreEqual(string.Empty, FileIndexItem.FixedListToString(null)); } - + [TestMethod] public void FixedListToString_One() { - Assert.AreEqual("test", FileIndexItem.FixedListToString(new List{"test"})); + Assert.AreEqual("test", FileIndexItem.FixedListToString(new List { "test" })); } - + [TestMethod] public void FixedListToString_Two() { - Assert.AreEqual("test|test2", FileIndexItem.FixedListToString(new List{"test","test2"})); + Assert.AreEqual("test|test2", + FileIndexItem.FixedListToString(new List { "test", "test2" })); } } } diff --git a/starsky/starskytest/starsky.foundation.database/QueryTest/QueryRemoveItemAsyncTest.cs b/starsky/starskytest/starsky.foundation.database/QueryTest/QueryRemoveItemAsyncTest.cs index 43b86ac328..08a5b47477 100644 --- a/starsky/starskytest/starsky.foundation.database/QueryTest/QueryRemoveItemAsyncTest.cs +++ b/starsky/starskytest/starsky.foundation.database/QueryTest/QueryRemoveItemAsyncTest.cs @@ -64,7 +64,7 @@ public async Task QueryRemoveItemAsyncTest_List_AddOneItem() } [TestMethod] - public async Task RemoveAsync_Disposed() + public async Task Query_RemoveAsync_Disposed() { var addedItems = new List { @@ -81,11 +81,11 @@ public async Task RemoveAsync_Disposed() // Dispose here await dbContextDisposed.DisposeAsync(); - - await new Query(dbContextDisposed, - new AppSettings { - AddMemoryCache = false - }, serviceScopeFactory, new FakeIWebLogger(), new FakeMemoryCache()).RemoveItemAsync(addedItems); + + var service = new Query(dbContextDisposed, + new AppSettings { AddMemoryCache = false }, serviceScopeFactory, new FakeIWebLogger(), + new FakeMemoryCache()); + await service.RemoveItemAsync(addedItems); var context = new InjectServiceScope(serviceScopeFactory).Context(); var queryFromDb = context.FileIndex.Where(p => diff --git a/starsky/starskytest/starsky.foundation.database/QueryTest/QueryTest.cs b/starsky/starskytest/starsky.foundation.database/QueryTest/QueryTest.cs index 3a905f58e1..bdb4e28c7d 100644 --- a/starsky/starskytest/starsky.foundation.database/QueryTest/QueryTest.cs +++ b/starsky/starskytest/starsky.foundation.database/QueryTest/QueryTest.cs @@ -12,7 +12,7 @@ using starsky.foundation.database.Query; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; -using starskycore.Attributes; +using starsky.project.web.Attributes; using starskytest.FakeMocks; namespace starskytest.starsky.foundation.database.QueryTest diff --git a/starsky/starskytest/Helpers/HttpClientHelperTest.cs b/starsky/starskytest/starsky.foundation.http/Helpers/HttpClientHelperTest.cs similarity index 80% rename from starsky/starskytest/Helpers/HttpClientHelperTest.cs rename to starsky/starskytest/starsky.foundation.http/Helpers/HttpClientHelperTest.cs index 2a3b71cdd0..a36e4179fd 100644 --- a/starsky/starskytest/Helpers/HttpClientHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.http/Helpers/HttpClientHelperTest.cs @@ -10,7 +10,7 @@ using starskytest.FakeCreateAn; using starskytest.FakeMocks; -namespace starskytest.Helpers +namespace starskytest.starsky.foundation.http.Helpers { [TestClass] public sealed class HttpClientHelperTest @@ -27,15 +27,16 @@ public async Task Download_HttpClientHelperBadDomainDownload() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // use only whitelisted domains var path = Path.Combine(new AppSettings().TempFolder, "pathToNOTdownload.txt"); - var output = await httpClientHelper.Download("http://mybadurl.cn",path); - Assert.AreEqual(false,output); + var output = await httpClientHelper.Download("http://mybadurl.cn", path); + Assert.AreEqual(false, output); } - + [TestMethod] public async Task Download_HttpClientHelper_404NotFoundTest() { @@ -48,15 +49,16 @@ public async Task Download_HttpClientHelper_404NotFoundTest() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // there is an file written var path = Path.Combine(new CreateAnImage().BasePath, "file.txt"); - var output = await httpClientHelper.Download("https://download.geonames.org/404",path); - Assert.AreEqual(false,output); + var output = await httpClientHelper.Download("https://download.geonames.org/404", path); + Assert.AreEqual(false, output); } - + [TestMethod] public async Task Download_HttpClientHelper_HTTP_Not_Download() { @@ -69,15 +71,16 @@ public async Task Download_HttpClientHelper_HTTP_Not_Download() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // http is not used anymore var path = Path.Combine(new AppSettings().TempFolder, "pathToNOTdownload.txt"); - var output = await httpClientHelper.Download("http://qdraw.nl",path); - Assert.AreEqual(false,output); + var output = await httpClientHelper.Download("http://qdraw.nl", path); + Assert.AreEqual(false, output); } - + [TestMethod] public async Task Download_HttpClientHelper_Download() { @@ -92,15 +95,17 @@ public async Task Download_HttpClientHelper_Download() var scopeFactory = serviceProvider.GetRequiredService(); var storageProvider = serviceProvider.GetRequiredService(); - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // there is an file written var path = Path.Combine(new CreateAnImage().BasePath, "file.txt"); - var output = await httpClientHelper.Download("https://qdraw.nl/test",path); - - Assert.AreEqual(true,output); - - Assert.AreEqual(FolderOrFileModel.FolderOrFileTypeList.File,storageProvider.IsFolderOrFile(path)); + var output = await httpClientHelper.Download("https://qdraw.nl/test", path); + + Assert.AreEqual(true, output); + + Assert.AreEqual(FolderOrFileModel.FolderOrFileTypeList.File, + storageProvider.IsFolderOrFile(path)); storageProvider.FileDelete(path); } @@ -109,18 +114,20 @@ public async Task Download_HttpClientHelper_Download() public async Task Download_HttpClientHelper_Download_HttpRequestException() { // > next HttpRequestException - var fakeHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("should fail")); + var fakeHttpMessageHandler = + new FakeHttpMessageHandler(new HttpRequestException("should fail")); var httpClient = new HttpClient(fakeHttpMessageHandler); var httpProvider = new HttpProvider(httpClient); - + var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); - var output = await httpClientHelper.Download("https://qdraw.nl/test","/sdkflndf",1); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + var output = await httpClientHelper.Download("https://qdraw.nl/test", "/sdkflndf", 1); Assert.IsFalse(output); } @@ -128,9 +135,10 @@ public async Task Download_HttpClientHelper_Download_HttpRequestException() [ExpectedException(typeof(EndOfStreamException))] public async Task Download_HttpClientHelper_Download_NoStorage() { - await new HttpClientHelper(new FakeIHttpProvider(), null, new FakeIWebLogger()).Download("t","T"); + await new HttpClientHelper(new FakeIHttpProvider(), null, new FakeIWebLogger()) + .Download("t", "T"); } - + [TestMethod] public async Task ReadString_HttpClientHelper_ReadString() { @@ -144,29 +152,32 @@ public async Task ReadString_HttpClientHelper_ReadString() var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper.ReadString("https://qdraw.nl/test"); - - Assert.AreEqual(true,output.Key); + + Assert.AreEqual(true, output.Key); } - - + + [TestMethod] public async Task ReadString_HttpClientHelper_ReadString_HttpRequestException() { // > next HttpRequestException - var fakeHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("should fail")); + var fakeHttpMessageHandler = + new FakeHttpMessageHandler(new HttpRequestException("should fail")); var httpClient = new HttpClient(fakeHttpMessageHandler); var httpProvider = new HttpProvider(httpClient); - + var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper.ReadString("https://qdraw.nl/test"); Assert.IsFalse(output.Key); } @@ -183,14 +194,15 @@ public async Task ReadString_HttpClientHelper_HTTP_Not_ReadString() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // http is not used anymore var output = await httpClientHelper.ReadString("http://qdraw.nl"); - Assert.AreEqual(false,output.Key); + Assert.AreEqual(false, output.Key); } - + [TestMethod] public async Task ReadString_HttpClientHelper_404NotFound_ReadString_Test() { @@ -203,13 +215,14 @@ public async Task ReadString_HttpClientHelper_404NotFound_ReadString_Test() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper.ReadString("https://download.geonames.org/404"); - Assert.AreEqual(false,output.Key); + Assert.AreEqual(false, output.Key); } - + [TestMethod] public async Task PostString_HttpClientHelper() { @@ -223,14 +236,15 @@ public async Task PostString_HttpClientHelper() var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper .PostString("https://qdraw.nl/test", new StringContent(string.Empty)); - - Assert.AreEqual(true,output.Key); + + Assert.AreEqual(true, output.Key); } - + [TestMethod] public async Task PostString_HttpClientHelper_VerboseFalse() { @@ -245,30 +259,34 @@ public async Task PostString_HttpClientHelper_VerboseFalse() var scopeFactory = serviceProvider.GetRequiredService(); var fakeLogger = new FakeIWebLogger(); - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory,fakeLogger); + var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, fakeLogger); await httpClientHelper - .PostString("https://qdraw.nl/test", new StringContent(string.Empty),false); - - Assert.IsFalse(fakeLogger.TrackedInformation.Exists(p => p.Item2?.Contains("PostString") == true)); - Assert.IsFalse(fakeLogger.TrackedInformation.Exists(p => p.Item2?.Contains("HttpClientHelper") == true)); + .PostString("https://qdraw.nl/test", new StringContent(string.Empty), false); + + Assert.IsFalse( + fakeLogger.TrackedInformation.Exists(p => p.Item2?.Contains("PostString") == true)); + Assert.IsFalse(fakeLogger.TrackedInformation.Exists(p => + p.Item2?.Contains("HttpClientHelper") == true)); } - + [TestMethod] public async Task PostString_HttpRequestException() { // > next HttpRequestException - var fakeHttpMessageHandler = new FakeHttpMessageHandler(new HttpRequestException("should fail")); + var fakeHttpMessageHandler = + new FakeHttpMessageHandler(new HttpRequestException("should fail")); var httpClient = new HttpClient(fakeHttpMessageHandler); var httpProvider = new HttpProvider(httpClient); - + var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper .PostString("https://qdraw.nl/test", new StringContent(string.Empty)); Assert.IsFalse(output.Key); @@ -286,15 +304,16 @@ public async Task PostString_HTTP_Not_ReadString() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); // http is not used anymore var output = await httpClientHelper .PostString("http://qdraw.nl", new StringContent(string.Empty)); - Assert.AreEqual(false,output.Key); + Assert.AreEqual(false, output.Key); } - + [TestMethod] public async Task PostString_404NotFound_Test() { @@ -307,14 +326,13 @@ public async Task PostString_404NotFound_Test() services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); var scopeFactory = serviceProvider.GetRequiredService(); - - var httpClientHelper = new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); + + var httpClientHelper = + new HttpClientHelper(httpProvider, scopeFactory, new FakeIWebLogger()); var output = await httpClientHelper .PostString("https://download.geonames.org/404", new StringContent(string.Empty)); - Assert.AreEqual(false,output.Key); + Assert.AreEqual(false, output.Key); } - - } } diff --git a/starsky/starskytest/starsky.foundation.native/Helpers/OperatingSystemHelperTest.cs b/starsky/starskytest/starsky.foundation.native/Helpers/OperatingSystemHelperTest.cs index 4c8152c8a2..a90020a17a 100644 --- a/starsky/starskytest/starsky.foundation.native/Helpers/OperatingSystemHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.native/Helpers/OperatingSystemHelperTest.cs @@ -13,9 +13,7 @@ public void OperatingSystemHelper1() var result = OperatingSystemHelper.GetPlatform(); Assert.IsNotNull(result); } - - - + [TestMethod] public void OperatingSystemHelper_Windows() { diff --git a/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrlTests.cs b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrlTests.cs new file mode 100644 index 0000000000..b4e095c08f --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/MacOsOpenUrlTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Medallion.Shell; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.native.Helpers; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starskytest.FakeCreateAn; + +namespace starskytest.starsky.foundation.native.OpenApplicationNative.Helpers; + +[TestClass] +public class MacOsOpenUrlTests +{ + [TestMethod] + public void OpenDefault_NonMacOS() + { + var result = MacOsOpenUrl.OpenDefault(["any value"], OSPlatform.Linux); + Assert.IsNull(result); + } + + [TestMethod] + [ExpectedException(typeof(DllNotFoundException))] + public void OpenDefault__NonMacOS() + { + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Windows / Linux only"); + return; + } + + MacOsOpenUrl.OpenDefault(["not important"], OSPlatform.OSX); + } + + + private const string ConsoleApp = "/System/Applications/Utilities/Console.app"; + private const string ConsoleName = "Console"; + + [TestMethod] + public async Task TestMethodWithSpecificApp__MacOnly() + { + if ( OperatingSystemHelper.GetPlatform() != OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Mac OS Only"); + return; + } + + var filePath = new CreateAnImage().FullFilePath; + + MacOsOpenUrl.OpenApplicationAtUrl([filePath], ConsoleApp); + + var isProcess = Process.GetProcessesByName(ConsoleName).ToList() + .Exists(p => p.MainModule?.FileName.Contains(ConsoleApp) == true); + + await Task.Delay(4); + + for ( var i = 0; i < 20; i++ ) + { + isProcess = Process.GetProcessesByName(ConsoleName).ToList() + .Exists(p => p.MainModule?.FileName.Contains(ConsoleApp) == true); + + if ( isProcess ) + { + await Command.Run("osascript", "-e", + "tell application \"Console\" to if it is running then quit").Task; + break; + } + + await Task.Delay(5); + } + + Assert.IsTrue(isProcess); + } + + [TestMethod] + public void OpenApplicationAtUrl_NoItems() + { + var result = MacOsOpenUrl.OpenApplicationAtUrl([], ConsoleApp); + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenDefault_NoItems() + { + var result = MacOsOpenUrl.OpenDefault([]); + Assert.IsFalse(result); + } + + [TestMethod] + public void TestMethodWithDefaultApp__MacOnly() + { + if ( OperatingSystemHelper.GetPlatform() != OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Mac OS Only"); + return; + } + + var result = MacOsOpenUrl.OpenDefault(["urlNotFound"]); + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenApplicationAtUrl_NonMacOs() + { + var result = MacOsOpenUrl.OpenApplicationAtUrl(new List { "any value" }, "app", + OSPlatform.Linux); + Assert.IsNull(result); + } + + [TestMethod] + [ExpectedException(typeof(DllNotFoundException))] + public void OpenApplicationAtUrl__NonMacOS() + { + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Windows / Linux only"); + return; + } + + MacOsOpenUrl.OpenApplicationAtUrl(["not important"], "not important", OSPlatform.OSX); + } + + [TestMethod] + [ExpectedException(typeof(DllNotFoundException))] + public void OpenURLsWithApplicationAtURL__NonMacOS() + { + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Windows / Linux only"); + return; + } + + MacOsOpenUrl.OpenUrLsWithApplicationAtUrl(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); + } + + [TestMethod] + [ExpectedException(typeof(DllNotFoundException))] + public void NsWorkspaceSharedWorkSpace__NonMacOS() + { + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Windows / Linux only"); + return; + } + + MacOsOpenUrl.NsWorkspaceSharedWorkSpace(); + } + + [TestMethod] + [ExpectedException(typeof(DllNotFoundException))] + public void InvokeOpenUrl__NonMacOS() + { + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.OSX ) + { + Assert.Inconclusive("This test if for Windows / Linux only"); + return; + } + + MacOsOpenUrl.InvokeOpenUrl(IntPtr.Zero); + } +} diff --git a/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopAppTests.cs b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopAppTests.cs new file mode 100644 index 0000000000..34d30a987b --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsOpenDesktopAppTests.cs @@ -0,0 +1,255 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Win32; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeCreateAn.CreateFakeStarskyExe; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Medallion.Shell; + +namespace starskytest.starsky.foundation.native.OpenApplicationNative.Helpers; + +[TestClass] +public class WindowsOpenDesktopAppTests +{ + private const string Extension = ".starsky"; + private const string ProgId = "starskytest"; + private const string FileTypeDescription = "Starsky Test File"; + + [TestInitialize] + public void TestInitialize() + { + SetupEnsureAssociationsSet(); + } + + [TestCleanup] + public void TestCleanup() + { + CleanSetup(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", + "CA1416:Validate platform compatibility", Justification = "Check does exists")] + private static void CleanSetup() + { + if ( !new AppSettings().IsWindows ) + { + return; + } + + // Ensure no keys exist before the test starts + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{Extension}", false); + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{ProgId}", false); + } + + private static CreateFakeStarskyWindowsExe SetupEnsureAssociationsSet() + { + if ( !new AppSettings().IsWindows ) + { + return new CreateFakeStarskyWindowsExe(); + } + + var mock = new CreateFakeStarskyWindowsExe(); + var filePath = mock.FullFilePath; + WindowsSetFileAssociations.EnsureAssociationsSet( + new FileAssociation + { + Extension = Extension, + ProgId = ProgId, + FileTypeDescription = FileTypeDescription, + ExecutableFilePath = filePath + }); + return mock; + } + + + [TestMethod] + public void W_OpenDefault_NonWindows() + { + var result = WindowsOpenDesktopApp.OpenDefault(["any value"], OSPlatform.Linux); + Assert.IsNull(result); + } + + [TestMethod] + public void W_OpenDefault2_NonWindows() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Unix Only"); + return; + } + + var result = WindowsOpenDesktopApp.OpenDefault(["any value"], OSPlatform.Windows); + + Assert.IsTrue(result); + } + + [TestMethod] + public void W_OpenDefault3_NonWindows() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Unix Only"); + return; + } + + var result = WindowsOpenDesktopApp.OpenDefault(["any value"]); + + Console.WriteLine(result); + + Assert.IsTrue(result); + } + + [TestMethod] + public async Task W_OpenDefault_HappyFlow__WindowsOnly() + { + if ( !new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Windows Only"); + return; + } + + var mock = SetupEnsureAssociationsSet(); + var result = + WindowsOpenDesktopApp.OpenDefault([mock.StarskyDotStarskyPath], OSPlatform.Windows); + + // retry due due multi threading + if ( result != true ) + { + Console.WriteLine("retry due due multi threading"); + await Task.Delay(100); + SetupEnsureAssociationsSet(); + result = WindowsOpenDesktopApp.OpenDefault([mock.StarskyDotStarskyPath]); + } + + Assert.IsTrue(result); + } + + [TestMethod] + public void W_OpenApplicationAtUrl_NonWindows() + { + var result = WindowsOpenDesktopApp.OpenApplicationAtUrl(new List { "any value" }, + "app", OSPlatform.Linux); + Assert.IsNull(result); + } + + [TestMethod] + [ExpectedException(typeof(Win32Exception))] + public void W_OpenApplicationAtUrl2_NonWindows() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Unix Only"); + return; + } + + // ExpectedException = Win32Exception + WindowsOpenDesktopApp.OpenApplicationAtUrl(["any value"], + "/not_found_849539453", OSPlatform.Windows); + } + + [TestMethod] + [ExpectedException(typeof(Win32Exception))] + public void W_OpenApplicationAtUrl3_NonWindows() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Unix Only"); + return; + } + + WindowsOpenDesktopApp.OpenApplicationAtUrl(new List { "any value" }, + "app"); + } + + [TestMethod] + public void W_OpenApplicationAtUrl_ReturnsTrue_WhenApplicationOpens__WindowsOnly() + { + if ( !new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Windows Only"); + return; + } + + // Arrange + var mock = new CreateFakeStarskyWindowsExe(); + + var fileUrls = new List { mock.StarskyDotStarskyPath, }; + + // Act + var result = WindowsOpenDesktopApp.OpenApplicationAtUrl(fileUrls, mock.FullFilePath); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public async Task W_OpenApplicationAtUrl_ReturnsTrue_WhenApplicationOpens__UnixOnly() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Unix Only"); + return; + } + + // Arrange + var mock = new CreateFakeStarskyUnixBash(); + var fileUrls = new List { mock.StarskyDotStarskyPath, }; + + await Command.Run("chmod", "+x", + mock.FullFilePath).Task; + + // Act + var result = WindowsOpenDesktopApp.OpenApplicationAtUrl(fileUrls, mock.FullFilePath); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public void W_OpenDefault_FileNotFound__WindowsOnly() + { + if ( !new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Windows Only"); + return; + } + + var result = WindowsOpenDesktopApp.OpenDefault(["C:\\not-found-74537587345853847345"], + OSPlatform.Windows); + Assert.IsFalse(result); + } + + [TestMethod] + public void WindowsOpenDesktopApp_OpenDefault_Count0() + { + var result = WindowsOpenDesktopApp.OpenDefault(new List()); + Assert.IsFalse(result); + } + + [TestMethod] + public void WindowsOpenDesktopApp_OpenDefault_Count0_OSLinux() + { + var result = WindowsOpenDesktopApp.OpenDefault([], OSPlatform.Linux); + Assert.IsNull(result); + } + + + [TestMethod] + public void WindowsOpenDesktopApp_OpenApplicationAtUrl_Count0() + { + var result = WindowsOpenDesktopApp.OpenApplicationAtUrl([], string.Empty); + Assert.IsFalse(result); + } + + [TestMethod] + public void WindowsOpenDesktopApp_OpenApplicationAtUrl_Count0_OSLinux() + { + var result = WindowsOpenDesktopApp.OpenApplicationAtUrl([], + string.Empty, OSPlatform.Linux); + Assert.IsNull(result); + } +} diff --git a/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsTests.cs b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsTests.cs new file mode 100644 index 0000000000..4d6f625180 --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsTests.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Win32; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeCreateAn.CreateFakeStarskyExe; +using System.Text.RegularExpressions; + +namespace starskytest.starsky.foundation.native.OpenApplicationNative.Helpers +{ + /// + /// Only for Windows - test the WindowsSetFileAssociationsWindows + /// + [TestClass] + public class WindowsSetFileAssociationsTests + { + private const string Extension = ".starsky"; + private const string ProgId = "starskytest"; + private const string FileTypeDescription = "Starsky Test File"; + + [TestInitialize] + public void TestInitialize() + { + CleanSetup(); + } + + [TestCleanup] + public void TestCleanup() + { + CleanSetup(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", + "CA1416:Validate platform compatibility", Justification = "Check does exists")] + private static void CleanSetup() + { + if ( !new AppSettings().IsWindows ) + { + return; + } + + // Ensure no keys exist before the test starts + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{Extension}", false); + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{ProgId}", false); + } + + [TestMethod] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", + "CA1416:Validate platform compatibility", Justification = "Check if test for windows only")] + public void EnsureAssociationsSet() + { + if ( !new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Windows Only"); + return; + } + + var filePath = new CreateFakeStarskyWindowsExe().FullFilePath; + WindowsSetFileAssociations.EnsureAssociationsSet( + new FileAssociation + { + Extension = Extension, + ProgId = ProgId, + FileTypeDescription = FileTypeDescription, + ExecutableFilePath = filePath + }); + + var registryKeyPath = $@"Software\Classes\{ProgId}\shell\open\command"; + + using var key = Registry.CurrentUser.OpenSubKey(registryKeyPath); + + var valueKey = key?.GetValue(string.Empty)?.ToString(); + var pattern = "\"([^\"]*)\""; + Assert.IsNotNull( valueKey ); + var match = Regex.Match(valueKey, pattern); + var value = match.Groups[1].Value; + + Assert.AreEqual(filePath, value); + } + } +} diff --git a/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsUnixTests.cs b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsUnixTests.cs new file mode 100644 index 0000000000..85b5b5697a --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/Helpers/WindowsSetFileAssociationsUnixTests.cs @@ -0,0 +1,46 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starsky.foundation.platform.Models; + +namespace starskytest.starsky.foundation.native.OpenApplicationNative.Helpers; + +/// +/// Only for non Windows - so no tests - This feature is windows specific +/// +[TestClass] +public class WindowsSetFileAssociationsUnixTests +{ + [TestMethod] + public void EnsureAssociationsSet__UnixOnly() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Mac and Linux Only"); + return; + } + + var result = WindowsSetFileAssociations.EnsureAssociationsSet(new FileAssociation + { + Extension = ".jpg", + ProgId = "starsky", + FileTypeDescription = "Starsky Test File", + ExecutableFilePath = "/usr/bin/starsky" + }); + + Assert.IsFalse(result); + } + + [TestMethod] + public void SetKeyDefaultValue__UnixOnly() + { + if ( new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Mac and Linux Only"); + return; + } + + // Is false due its unix + var result = WindowsSetFileAssociations.SetKeyValue("test", "Test"); + Assert.IsFalse(result); + } +} diff --git a/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeServiceTest.cs b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeServiceTest.cs new file mode 100644 index 0000000000..26ecc336fa --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/OpenApplicationNative/OpenApplicationNativeServiceTest.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Win32; +using starsky.foundation.native.Helpers; +using starsky.foundation.native.OpenApplicationNative; +using starsky.foundation.native.OpenApplicationNative.Helpers; +using starsky.foundation.platform.Models; +using starskytest.FakeCreateAn.CreateFakeStarskyExe; +using starskytest.starsky.foundation.native.Helpers; + +namespace starskytest.starsky.foundation.native.OpenApplicationNative; + +[TestClass] +public class OpenApplicationNativeServiceTest +{ + private const string Extension = ".starsky"; + private const string ProgramId = "starskytest"; + private const string FileTypeDescription = "Starsky Test File"; + + [TestInitialize] + public void TestInitialize() + { + SetupEnsureAssociationsSet(); + } + + private static CreateFakeStarskyWindowsExe SetupEnsureAssociationsSet() + { + if ( !new AppSettings().IsWindows ) + { + return new CreateFakeStarskyWindowsExe(); + } + + var mock = new CreateFakeStarskyWindowsExe(); + var filePath = mock.FullFilePath; + WindowsSetFileAssociations.EnsureAssociationsSet( + new FileAssociation + { + Extension = Extension, + ProgId = ProgramId, + FileTypeDescription = FileTypeDescription, + ExecutableFilePath = filePath + }); + return mock; + } + + [TestCleanup] + public void TestCleanup() + { + CleanSetup(); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", + "CA1416:Validate platform compatibility", Justification = "Check does exists")] + private static void CleanSetup() + { + if ( !new AppSettings().IsWindows ) + { + return; + } + + // Ensure no keys exist before the test starts + + try + { + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{Extension}", false); + Registry.CurrentUser.DeleteSubKeyTree($"Software\\Classes\\{ProgramId}", false); + } + catch ( IOException ) + { + // do nothing + } + } + + [TestMethod] + public async Task Service_OpenDefault_HappyFlow__WindowsOnly() + { + if ( !new AppSettings().IsWindows ) + { + Assert.Inconclusive("This test if for Windows Only"); + return; + } + + var mock = SetupEnsureAssociationsSet(); + + var result = + new OpenApplicationNativeService().OpenDefault([mock.StarskyDotStarskyPath]); + + // retry due due multi threading + if ( result != true ) + { + Console.WriteLine("retry due due multi threading"); + await Task.Delay(100); + SetupEnsureAssociationsSet(); + var service = new OpenApplicationNativeService(); + result = service.OpenDefault([mock.StarskyDotStarskyPath]); + } + + Assert.IsTrue(result); + } + + + [TestMethod] + public void OpenApplicationAtUrl_ZeroItems_SoFalse() + { + var result = OpenApplicationNativeService.OpenApplicationAtUrl([], "app"); + + // Linux and FreeBSD are not supported + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.Linux || + OperatingSystemHelper.GetPlatform() == OSPlatform.FreeBSD ) + { + Assert.IsNull(result); + return; + } + + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenDefault_ZeroItemsSo_False() + { + var result = new OpenApplicationNativeService().OpenDefault([]); + + // Linux and FreeBSD are not supported + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.Linux || + OperatingSystemHelper.GetPlatform() == OSPlatform.FreeBSD ) + { + Assert.IsNull(result); + return; + } + + Assert.IsFalse(result); + } + + [TestMethod] + public void OpenApplicationAtUrl_AllApplicationsSupported_ReturnsTrue__LinuxOnly() + { + if ( OperatingSystemHelper.GetPlatform() != OSPlatform.Linux ) + { + Assert.Inconclusive("This test if for Linux Only"); + return; + } + + // Arrange + var service = new OpenApplicationNativeService(); + // List is (File Path and Application URL) + + var fullPathAndApplicationUrl = new List<(string, string)> + { + ( new CreateFakeStarskyUnixBash().StarskyDotStarskyPath, + new CreateFakeStarskyUnixBash().ApplicationUrl ) + }; + + // Act + var result = service.OpenApplicationAtUrl(fullPathAndApplicationUrl); + + // Assert + Assert.IsNull(result); + } + + + [TestMethod] + public void OpenApplicationAtUrl_NoApplications_ReturnsFalse() + { + // Arrange + var service = new OpenApplicationNativeService(); + var fullPathAndApplicationUrl = new List<(string, string)>(); + + // Act + var result = service.OpenApplicationAtUrl(fullPathAndApplicationUrl); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void SortToOpenFilesByApplicationPath_EmptyList_ReturnsEmptyList() + { + // Arrange + var fullPathAndApplicationUrl = new List<(string, string)>(); + + // Act + var result = + OpenApplicationNativeService + .SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl); + + // Assert + Assert.AreEqual(0, result.Count); + } + + [TestMethod] + public void SortToOpenFilesByApplicationPath_SingleApplication_ReturnsSingleGroup() + { + // Arrange + var fullPathAndApplicationUrl = new List<(string, string)> + { + ( "file1", "app1" ), ( "file2", "app1" ), ( "file3", "app1" ) + }; + + // Act + var result = + OpenApplicationNativeService + .SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl); + + // Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual(3, result[0].Item1.Count); + Assert.AreEqual("app1", result[0].Item2); + } + + [TestMethod] + public void SortToOpenFilesByApplicationPath_MultipleApplications_ReturnsMultipleGroups() + { + // Arrange + var fullPathAndApplicationUrl = new List<(string, string)> + { + ( "file1", "app1" ), + ( "file2", "app2" ), + ( "file3", "app1" ), + ( "file4", "app2" ), + ( "file5", "app3" ) + }; + + // Act + var result = + OpenApplicationNativeService + .SortToOpenFilesByApplicationPath(fullPathAndApplicationUrl); + + // Assert + Assert.AreEqual(3, result.Count); + Assert.IsTrue(result.Exists(x => x.Item2 == "app1")); + Assert.IsTrue(result.Exists(x => x.Item2 == "app2")); + Assert.IsTrue(result.Exists(x => x.Item2 == "app3")); + } + + [TestMethod] + public void DetectToUseOpenApplication_Default() + { + var result = new OpenApplicationNativeService().DetectToUseOpenApplication(); + + // Depending on the environment + if ( !Environment.UserInteractive && new AppSettings().IsWindows ) + { + Assert.IsFalse(result); + return; + } + + // Linux and FreeBSD are not supported + if ( OperatingSystemHelper.GetPlatform() == OSPlatform.Linux || + OperatingSystemHelper.GetPlatform() == OSPlatform.FreeBSD ) + { + Assert.IsFalse(result); + return; + } + + Assert.IsTrue(result); + } + + [TestMethod] + public void DetectToUseOpenApplicationInternal_Windows_AsWindowsService_InteractiveFalse() + { + var result = + OpenApplicationNativeService.DetectToUseOpenApplicationInternal( + FakeOsOverwrite.IsWindows, + false); + Assert.IsFalse(result); + } + + [TestMethod] + public void DetectToUseOpenApplicationInternal_MacOS_AsLaunchService_InteractiveTrue() + { + var result = + OpenApplicationNativeService.DetectToUseOpenApplicationInternal(FakeOsOverwrite.IsMacOs, + false); + Assert.IsTrue(result); + } + + [TestMethod] + public void DetectToUseOpenApplicationInternal_MacOS_Interactive_InteractiveTrue() + { + var result = + OpenApplicationNativeService.DetectToUseOpenApplicationInternal(FakeOsOverwrite.IsMacOs, + true); + Assert.IsTrue(result); + } + + + [TestMethod] + public void DetectToUseOpenApplicationInternal_Linux_Interactive_Interactive_False() + { + var result = + OpenApplicationNativeService.DetectToUseOpenApplicationInternal(FakeOsOverwrite.IsLinux, + true); + Assert.IsFalse(result); + } +} diff --git a/starsky/starskytest/starsky.foundation.native/Trash/Helpers/Example/Clipboard.cs b/starsky/starskytest/starsky.foundation.native/Trash/Helpers/Example/Clipboard.cs new file mode 100644 index 0000000000..7cc37b4656 --- /dev/null +++ b/starsky/starskytest/starsky.foundation.native/Trash/Helpers/Example/Clipboard.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace starskytest.starsky.foundation.native.Trash.Helpers.Example; + +[SuppressMessage("Interoperability", "SYSLIB1054:Use \'LibraryImportAttribute\' " + + "instead of \'DllImportAttribute\' to generate P/Invoke " + + "marshalling code at compile time")] +[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments")] +public static class Clipboard +{ + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, string arg1); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg1); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr sel_registerName(string selectorName); + + [DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")] + private static extern IntPtr objc_getClass(string className); + + public static string? GetText() + { + var nsString = objc_getClass("NSString"); + var nsPasteboard = objc_getClass("NSPasteboard"); + + var nsStringPboardType = objc_msgSend(objc_msgSend(nsString, sel_registerName("alloc")), + sel_registerName("initWithUTF8String:"), "NSStringPboardType"); + var generalPasteboard = objc_msgSend(nsPasteboard, sel_registerName("generalPasteboard")); + var ptr = objc_msgSend(generalPasteboard, sel_registerName("stringForType:"), + nsStringPboardType); + var charArray = objc_msgSend(ptr, sel_registerName("UTF8String")); + return Marshal.PtrToStringAnsi(charArray); + } +} diff --git a/starsky/starskytest/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelperTest.cs b/starsky/starskytest/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelperTest.cs index 67ef6d98a7..ad57a26f11 100644 --- a/starsky/starskytest/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.native/Trash/Helpers/WindowsShellTrashBindingHelperTest.cs @@ -239,6 +239,7 @@ public void DriveHasRecycleBin_C_Drive() Assert.AreEqual(0, items); Assert.AreEqual(false, driveHasBin); Assert.IsTrue(info.Contains("Unable to load shared library")); + Assert.Inconclusive("Shell32.dll is not available on Linux or Mac OS"); return; } diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/AppSettingsCompareHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/AppSettingsCompareHelperTest.cs index ca27847264..ba0e7fb708 100644 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/AppSettingsCompareHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/AppSettingsCompareHelperTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; @@ -17,7 +18,7 @@ public void NewObject() Assert.AreEqual(input.Structure, new AppSettings().Structure); } - + [TestMethod] public void StringCompare() { @@ -26,7 +27,7 @@ public void StringCompare() DatabaseType = AppSettings.DatabaseTypeList.Sqlite, DatabaseConnection = "Data Source=source" }; - + var to = new AppSettings { DatabaseType = AppSettings.DatabaseTypeList.Sqlite, @@ -36,15 +37,12 @@ public void StringCompare() AppSettingsCompareHelper.Compare(source, to); Assert.AreEqual(source.DatabaseConnection, to.DatabaseConnection); } - + [TestMethod] public void NullableBoolCompare() { - var source = new AppSettings - { - Verbose = true - }; - + var source = new AppSettings { Verbose = true }; + var to = new AppSettingsTransferObject() { Verbose = false // or null @@ -53,60 +51,82 @@ public void NullableBoolCompare() AppSettingsCompareHelper.Compare(source, to); Assert.AreEqual(source.Verbose, to.Verbose); } - + [TestMethod] public void ListStringCompare() { - var source = new AppSettings - { - ReadOnlyFolders = new List{"/test"} - }; - + var source = new AppSettings { ReadOnlyFolders = new List { "/test" } }; + + var to = new AppSettings { ReadOnlyFolders = new List { "/test2" } }; + + AppSettingsCompareHelper.Compare(source, to); + Assert.AreEqual(source.ReadOnlyFolders.FirstOrDefault(), + to.ReadOnlyFolders.FirstOrDefault()); + } + + [TestMethod] + public void ListStringCompare_Same() + { + var source = new AppSettings { ReadOnlyFolders = new List { "/same" } }; + + var to = new AppSettings { ReadOnlyFolders = new List { "/same" } }; + + var compare = AppSettingsCompareHelper.Compare(source, source); + + Assert.AreEqual(source.ReadOnlyFolders.FirstOrDefault(), + to.ReadOnlyFolders.FirstOrDefault()); + Assert.AreEqual(0, compare.Count); + } + + [TestMethod] + public void DatabaseTypeListCompare() + { + var source = new AppSettings { DatabaseType = AppSettings.DatabaseTypeList.Sqlite }; + var to = new AppSettings { - ReadOnlyFolders = new List{"/test2"} + DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase }; AppSettingsCompareHelper.Compare(source, to); - Assert.AreEqual(source.ReadOnlyFolders.FirstOrDefault(), to.ReadOnlyFolders.FirstOrDefault()); + Assert.AreEqual(source.DatabaseType, to.DatabaseType); } - + [TestMethod] - public void ListStringCompare_Same() + public void DesktopCollectionsOpenCompare() { var source = new AppSettings { - ReadOnlyFolders = new List{"/same"} + DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; var to = new AppSettings { - ReadOnlyFolders = new List{"/same"} + DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Jpeg }; - var compare = AppSettingsCompareHelper.Compare(source, source); - - Assert.AreEqual(source.ReadOnlyFolders.FirstOrDefault(), to.ReadOnlyFolders.FirstOrDefault()); - Assert.AreEqual(0, compare.Count); + AppSettingsCompareHelper.Compare(source, to); + Assert.AreEqual(source.DesktopCollectionsOpen, to.DesktopCollectionsOpen); } - + [TestMethod] - public void DatabaseTypeListCompare() + public void DesktopCollectionsOpenCompare_DefaultIgnore() { var source = new AppSettings { - DatabaseType = AppSettings.DatabaseTypeList.Sqlite + DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Raw }; - + var to = new AppSettings { - DatabaseType = AppSettings.DatabaseTypeList.InMemoryDatabase + DesktopCollectionsOpen = CollectionsOpenType.RawJpegMode.Default }; AppSettingsCompareHelper.Compare(source, to); - Assert.AreEqual(source.DatabaseType, to.DatabaseType); + + Assert.AreEqual(CollectionsOpenType.RawJpegMode.Raw, source.DesktopCollectionsOpen); } - + [TestMethod] public void ListAppSettingsPublishProfilesCompare() { @@ -114,46 +134,52 @@ public void ListAppSettingsPublishProfilesCompare() { PublishProfiles = new Dictionary> { - {"zz__example", new List { - new AppSettingsPublishProfiles + "zz__example", new List { - ContentType = TemplateContentType.Jpeg, - SourceMaxWidth = 1000, - OverlayMaxWidth = 380, - Path = "{AssemblyDirectory}/EmbeddedViews/qdrawlarge.png", - Folder = "1000", - Append = "_kl1k" + new AppSettingsPublishProfiles + { + ContentType = TemplateContentType.Jpeg, + SourceMaxWidth = 1000, + OverlayMaxWidth = 380, + Path = + "{AssemblyDirectory}/EmbeddedViews/qdrawlarge.png", + Folder = "1000", + Append = "_kl1k" + } } - }} + } } }; - + var to = new AppSettings { PublishProfiles = new Dictionary> { - {"zz__example2", new List { - new AppSettingsPublishProfiles + "zz__example2", + new List { - ContentType = TemplateContentType.Jpeg, - SourceMaxWidth = 300, - OverlayMaxWidth = 380, - Folder = "1000", - Append = "_kl1k" + new AppSettingsPublishProfiles + { + ContentType = TemplateContentType.Jpeg, + SourceMaxWidth = 300, + OverlayMaxWidth = 380, + Folder = "1000", + Append = "_kl1k" + } } - }} + } } }; var compare = AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.PublishProfiles.Keys.FirstOrDefault(), to.PublishProfiles.Keys.FirstOrDefault()); - Assert.AreEqual("PublishProfiles".ToLowerInvariant(), compare.FirstOrDefault()); + Assert.AreEqual(source.PublishProfiles.Keys.FirstOrDefault(), + to.PublishProfiles.Keys.FirstOrDefault()); + Assert.AreEqual("PublishProfiles".ToLowerInvariant(), compare.FirstOrDefault()); } - + [TestMethod] public void ListAppSettingsStringDictionary_Changed() { @@ -161,24 +187,26 @@ public void ListAppSettingsStringDictionary_Changed() { AccountRolesByEmailRegisterOverwrite = new Dictionary { - {"zz__example2", "Administrator" - }} + { "zz__example2", "Administrator" } + } }; - + var to = new AppSettings { AccountRolesByEmailRegisterOverwrite = new Dictionary { - {"zz__example2", "User" - }} + { "zz__example2", "User" } + } }; var compare = AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault(), to.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault()); - Assert.AreEqual("AccountRolesByEmailRegisterOverwrite".ToLowerInvariant(), compare.FirstOrDefault()); + + Assert.AreEqual(source.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault(), + to.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault()); + Assert.AreEqual("AccountRolesByEmailRegisterOverwrite".ToLowerInvariant(), + compare.FirstOrDefault()); } - + [TestMethod] public void ListAppSettingsStringDictionary_Equal() { @@ -186,21 +214,23 @@ public void ListAppSettingsStringDictionary_Equal() { AccountRolesByEmailRegisterOverwrite = new Dictionary { - {"zz__example2", "Administrator" - }} + { "zz__example2", "Administrator" } + } }; - + var to = new AppSettings { - AccountRolesByEmailRegisterOverwrite = source.AccountRolesByEmailRegisterOverwrite + AccountRolesByEmailRegisterOverwrite = + source.AccountRolesByEmailRegisterOverwrite }; var compare = AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault(), to.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault()); + var expected = source.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault(); + var actual = to.AccountRolesByEmailRegisterOverwrite.Keys.FirstOrDefault(); + Assert.AreEqual(expected, actual); Assert.AreEqual(0, compare.Count); } - + [TestMethod] public void ListAppSettingsStringDictionary_IgnoreOverwrite() { @@ -208,20 +238,18 @@ public void ListAppSettingsStringDictionary_IgnoreOverwrite() { AccountRolesByEmailRegisterOverwrite = new Dictionary { - {"zz__example2", "Administrator" - }} - }; - - var to = new AppSettings - { - AccountRolesByEmailRegisterOverwrite = null + { "zz__example2", "Administrator" } + } }; + var to = new AppSettings { AccountRolesByEmailRegisterOverwrite = null }; + AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(null, to.AccountRolesByEmailRegisterOverwrite?.Keys.FirstOrDefault()); + + var actual = to.AccountRolesByEmailRegisterOverwrite?.Keys.FirstOrDefault(); + Assert.IsNull(actual); } - + [TestMethod] public void KeyValuePairStringString_Changed() { @@ -229,34 +257,28 @@ public void KeyValuePairStringString_Changed() { DemoData = new List { - new AppSettingsKeyValue - { - Key = "1", - Value = "2" - } + new AppSettingsKeyValue { Key = "1", Value = "2" } } }; - + var to = new AppSettings { DemoData = new List { - new AppSettingsKeyValue - { - Key = "3", - Value = "4" - } + new AppSettingsKeyValue { Key = "3", Value = "4" } } }; var compare = AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.DemoData.FirstOrDefault()?.Key, to.DemoData.FirstOrDefault()?.Key); - Assert.AreEqual(source.DemoData.FirstOrDefault()?.Value, to.DemoData.FirstOrDefault()?.Value); + + Assert.AreEqual(source.DemoData.FirstOrDefault()?.Key, + to.DemoData.FirstOrDefault()?.Key); + Assert.AreEqual(source.DemoData.FirstOrDefault()?.Value, + to.DemoData.FirstOrDefault()?.Value); Assert.AreEqual("DemoData".ToLowerInvariant(), compare.FirstOrDefault()); } - + [TestMethod] public void KeyValuePairStringString_Equal() { @@ -264,26 +286,21 @@ public void KeyValuePairStringString_Equal() { DemoData = new List { - new AppSettingsKeyValue - { - Key = "1", - Value = "2" - } + new AppSettingsKeyValue { Key = "1", Value = "2" } } }; - - var to = new AppSettings - { - DemoData = source.DemoData - }; + + var to = new AppSettings { DemoData = source.DemoData }; var compare = AppSettingsCompareHelper.Compare(source, to); - Assert.AreEqual(source.DemoData.FirstOrDefault()?.Key, to.DemoData.FirstOrDefault()?.Key); - Assert.AreEqual(source.DemoData.FirstOrDefault()?.Value, to.DemoData.FirstOrDefault()?.Value); + Assert.AreEqual(source.DemoData.FirstOrDefault()?.Key, + to.DemoData.FirstOrDefault()?.Key); + Assert.AreEqual(source.DemoData.FirstOrDefault()?.Value, + to.DemoData.FirstOrDefault()?.Value); Assert.AreEqual(0, compare.Count); } - + [TestMethod] public void KeyValuePairStringString_IgnoreOverwrite() { @@ -291,26 +308,19 @@ public void KeyValuePairStringString_IgnoreOverwrite() { DemoData = new List { - new AppSettingsKeyValue - { - Key = "1", - Value = "2" - } + new AppSettingsKeyValue { Key = "1", Value = "2" } } }; - - var to = new AppSettings - { - DemoData = null! - }; + + var to = new AppSettings { DemoData = null! }; var compare = AppSettingsCompareHelper.Compare(source, to); - + Assert.IsNull(to.DemoData); Assert.AreEqual(0, compare.Count); } - + [TestMethod] public void AppSettingsKeyValue_Compare() { @@ -318,32 +328,24 @@ public void AppSettingsKeyValue_Compare() { DemoData = new List { - new AppSettingsKeyValue - { - Key = "2", - Value = "1" - } + new AppSettingsKeyValue { Key = "2", Value = "1" } } }; - + var to = new AppSettings { DemoData = new List { - new AppSettingsKeyValue - { - Key = "1", - Value = "1" - } + new AppSettingsKeyValue { Key = "1", Value = "1" } } }; AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.PublishProfiles?.Keys.FirstOrDefault(), + + Assert.AreEqual(source.PublishProfiles?.Keys.FirstOrDefault(), to.PublishProfiles?.Keys.FirstOrDefault()); } - + [TestMethod] public void AppSettingsKeyValue_Compare_Same() { @@ -351,25 +353,18 @@ public void AppSettingsKeyValue_Compare_Same() { DemoData = new List { - new AppSettingsKeyValue - { - Key = "same", - Value = "1" - } + new AppSettingsKeyValue { Key = "same", Value = "1" } } }; - - var to = new AppSettings - { - DemoData = source.DemoData - }; + + var to = new AppSettings { DemoData = source.DemoData }; AppSettingsCompareHelper.Compare(source, to); - - Assert.AreEqual(source.PublishProfiles?.Keys.FirstOrDefault(), + + Assert.AreEqual(source.PublishProfiles?.Keys.FirstOrDefault(), to.PublishProfiles?.Keys.FirstOrDefault()); } - + [TestMethod] public void ListAppSettingsPublishProfilesCompare_Same() { @@ -377,40 +372,47 @@ public void ListAppSettingsPublishProfilesCompare_Same() { PublishProfiles = new Dictionary> { - {"same", new List { - new AppSettingsPublishProfiles + "same", + new List { - ContentType = TemplateContentType.Jpeg, - SourceMaxWidth = 300, - OverlayMaxWidth = 380, - Folder = "1000", - Append = "_kl1k" + new AppSettingsPublishProfiles + { + ContentType = TemplateContentType.Jpeg, + SourceMaxWidth = 300, + OverlayMaxWidth = 380, + Folder = "1000", + Append = "_kl1k" + } } - }} + } } }; - + var to = new AppSettings { PublishProfiles = new Dictionary> { - {"same", new List { - new AppSettingsPublishProfiles + "same", + new List { - ContentType = TemplateContentType.Jpeg, - SourceMaxWidth = 300, - OverlayMaxWidth = 380, - Folder = "1000", - Append = "_kl1k" + new AppSettingsPublishProfiles + { + ContentType = TemplateContentType.Jpeg, + SourceMaxWidth = 300, + OverlayMaxWidth = 380, + Folder = "1000", + Append = "_kl1k" + } } - }} + } } }; AppSettingsCompareHelper.Compare(source, to); - Assert.AreEqual(source.PublishProfiles.Keys.FirstOrDefault(), to.PublishProfiles.Keys.FirstOrDefault()); + Assert.AreEqual(source.PublishProfiles.Keys.FirstOrDefault(), + to.PublishProfiles.Keys.FirstOrDefault()); } [TestMethod] @@ -423,18 +425,18 @@ public void CompareDatabaseTypeList_NotFound() AppSettings.DatabaseTypeList.Mysql, list); Assert.IsNotNull(list); } - + [TestMethod] public void CompareListString_NotFound() { var list = new List(); AppSettingsCompareHelper.CompareListString("t", new AppSettings(), - new List{"1"}, - new List{"1"}, list); + new List { "1" }, + new List { "1" }, list); Assert.IsNotNull(list); } - + [TestMethod] public void CompareListPublishProfiles_NotFound() { @@ -445,7 +447,7 @@ public void CompareListPublishProfiles_NotFound() new Dictionary>(), list); Assert.IsNotNull(list); } - + [TestMethod] public void CompareBool_NotFound() { @@ -457,7 +459,7 @@ public void CompareBool_NotFound() boolValue, list); Assert.IsNotNull(list); } - + [TestMethod] public void CompareString_NotFound() { @@ -468,7 +470,7 @@ public void CompareString_NotFound() "test", list); Assert.IsNotNull(list); } - + [TestMethod] public void CompareInt_NotFound() { @@ -479,7 +481,7 @@ public void CompareInt_NotFound() 2, list); Assert.IsNotNull(list); } - + [TestMethod] public void OpenTelemetrySettings() { @@ -496,7 +498,7 @@ public void OpenTelemetrySettings() LogsHeader = "source/logs" } }; - + var to = new AppSettings { OpenTelemetry = new OpenTelemetrySettings @@ -512,7 +514,7 @@ public void OpenTelemetrySettings() }; AppSettingsCompareHelper.Compare(source, to); - + Assert.AreEqual(source.OpenTelemetry.Header, to.OpenTelemetry.Header); Assert.AreEqual(source.OpenTelemetry.TracesEndpoint, to.OpenTelemetry.TracesEndpoint); Assert.AreEqual(source.OpenTelemetry.TracesHeader, to.OpenTelemetry.TracesHeader); @@ -520,7 +522,6 @@ public void OpenTelemetrySettings() Assert.AreEqual(source.OpenTelemetry.MetricsHeader, to.OpenTelemetry.MetricsHeader); Assert.AreEqual(source.OpenTelemetry.LogsEndpoint, to.OpenTelemetry.LogsEndpoint); Assert.AreEqual(source.OpenTelemetry.LogsHeader, to.OpenTelemetry.LogsHeader); - } [TestMethod] @@ -539,18 +540,81 @@ public void OpenTelemetrySettings_Ignore_DefaultOption() LogsHeader = "source/logs" } }; - - var to = new AppSettings - { - OpenTelemetry = new OpenTelemetrySettings() - }; + + var to = new AppSettings { OpenTelemetry = new OpenTelemetrySettings() }; AppSettingsCompareHelper.Compare(source, to); - + Assert.AreEqual("source/test", source.OpenTelemetry.Header); Assert.AreEqual("source/traces", source.OpenTelemetry.TracesEndpoint); Assert.AreEqual("source/metrics", source.OpenTelemetry.MetricsEndpoint); Assert.AreEqual("source/logs", source.OpenTelemetry.LogsEndpoint); } + + [TestMethod] + public void AppSettingsDefaultEditorApplication() + { + var source = new AppSettings + { + DefaultDesktopEditor = + [ + new AppSettingsDefaultEditorApplication() + { + ImageFormats = + [ + ExtensionRolesHelper.ImageFormat.bmp, + ExtensionRolesHelper.ImageFormat.jpg + ], + ApplicationPath = "source/test" + } + ] + }; + + var to = new AppSettings + { + DefaultDesktopEditor = + [ + new AppSettingsDefaultEditorApplication() + { + ImageFormats = [ExtensionRolesHelper.ImageFormat.jpg], + ApplicationPath = "to/test" + } + ] + }; + + AppSettingsCompareHelper.Compare(source, to); + + Assert.AreEqual(source.DefaultDesktopEditor.Count, to.DefaultDesktopEditor.Count); + Assert.AreEqual(source.DefaultDesktopEditor[0].ApplicationPath, + to.DefaultDesktopEditor[0].ApplicationPath); + Assert.AreEqual(source.DefaultDesktopEditor[0].ImageFormats, + to.DefaultDesktopEditor[0].ImageFormats); + } + + [TestMethod] + public void AppSettingsDefaultEditorApplication_Ignore_DefaultOption() + { + var source = new AppSettings + { + DefaultDesktopEditor = + [ + new AppSettingsDefaultEditorApplication() + { + ImageFormats = + [ + ExtensionRolesHelper.ImageFormat.bmp, + ExtensionRolesHelper.ImageFormat.jpg + ], + ApplicationPath = "source/test" + } + ] + }; + + var to = new AppSettings { DefaultDesktopEditor = [] }; + + AppSettingsCompareHelper.Compare(source, to); + + Assert.AreEqual(0, to.DefaultDesktopEditor.Count); + } } } diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/ArgsHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/ArgsHelperTest.cs index c6237dca0a..68d51a7795 100644 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/ArgsHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/ArgsHelperTest.cs @@ -8,7 +8,7 @@ using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; -using starskycore.Attributes; +using starsky.project.web.Attributes; using starskytest.FakeCreateAn; using starskytest.FakeMocks; @@ -29,11 +29,10 @@ public ArgsHelperTest() var newImage = new CreateAnImage(); var dict = new Dictionary { - { "App:StorageFolder", newImage.BasePath }, - { "App:Verbose", "true" } + { "App:StorageFolder", newImage.BasePath }, { "App:Verbose", "true" } }; // Start using dependency injection - var builder = new ConfigurationBuilder(); + var builder = new ConfigurationBuilder(); // Add random config to dependency injection builder.AddInMemoryCollection(dict); // build config @@ -45,86 +44,85 @@ public ArgsHelperTest() // get the service _appSettings = serviceProvider.GetRequiredService(); } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_NeedVerboseTest() { - var args = new List {"-v"}.ToArray(); + var args = new List { "-v" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedVerbose(args)); - + // Bool parse check - args = new List {"-v","true"}.ToArray(); + args = new List { "-v", "true" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedVerbose(args)); } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_NeedRecruisiveTest() { - var args = new List {"-r"}.ToArray(); + var args = new List { "-r" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedRecursive(args)); - + // Bool parse check - args = new List {"-r","true"}.ToArray(); + args = new List { "-r", "true" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedRecursive(args)); } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_NeedCacheCleanupTest() { - var args = new List {"-x"}.ToArray(); + var args = new List { "-x" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedCleanup(args)); - + // Bool parse check - args = new List {"-x","true"}.ToArray(); - Assert.IsTrue( ArgsHelper.NeedCleanup(args)); + args = new List { "-x", "true" }.ToArray(); + Assert.IsTrue(ArgsHelper.NeedCleanup(args)); } - - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_GetIndexModeTest() { // Default on so testing off - var args = new List {"-i","false"}.ToArray(); + var args = new List { "-i", "false" }.ToArray(); Assert.IsFalse(ArgsHelper.GetIndexMode(args)); } - - + + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_NeedHelpTest() { - var args = new List {"-h"}.ToArray(); + var args = new List { "-h" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedHelp(args)); // Bool parse cheArgsHelper_GetPath_CurrentDirectory_Testck - args = new List {"-h","true"}.ToArray(); + args = new List { "-h", "true" }.ToArray(); Assert.IsTrue(ArgsHelper.NeedHelp(args)); } - + [TestMethod] public void ArgsHelper_GetPathFormArgsTest() { - var args = new List {"-p", "/"}.ToArray(); + var args = new List { "-p", "/" }.ToArray(); Assert.AreEqual("/", new ArgsHelper(_appSettings).GetPathFormArgs(args)); } - + [TestMethod] public void GetUserInputPassword() { - var args = new List {"-p", "test"}.ToArray(); - Assert.AreEqual("test",ArgsHelper.GetUserInputPassword(args)); + var args = new List { "-p", "test" }.ToArray(); + Assert.AreEqual("test", ArgsHelper.GetUserInputPassword(args)); } - + [TestMethod] public void GetUserInputPasswordLong() { - var args = new List {"--password", "test"}.ToArray(); - Assert.AreEqual("test",ArgsHelper.GetUserInputPassword(args)); + var args = new List { "--password", "test" }.ToArray(); + Assert.AreEqual("test", ArgsHelper.GetUserInputPassword(args)); } [TestMethod] @@ -132,45 +130,46 @@ public void GetUserInputPasswordLong() public void ArgsHelper_GetPathFormArgsTest_FieldAccessException() { // inject appSettings! - var args = new List {"-p", "/"}.ToArray(); + var args = new List { "-p", "/" }.ToArray(); new ArgsHelper(null!).GetPathFormArgs(args); } - + [TestMethod] public void GetPathListFormArgsTest_SingleItem() { - var args = new List {"-p", "/"}.ToArray(); - Assert.AreEqual("/",new ArgsHelper(_appSettings).GetPathListFormArgs(args).FirstOrDefault()); + var args = new List { "-p", "/" }.ToArray(); + Assert.AreEqual("/", + new ArgsHelper(_appSettings).GetPathListFormArgs(args).FirstOrDefault()); } - + [TestMethod] public void GetPathListFormArgsTest_MultipleItems() { - var args = new List {"-p", "\"/;/test\""}.ToArray(); + var args = new List { "-p", "\"/;/test\"" }.ToArray(); var result = new ArgsHelper(_appSettings).GetPathListFormArgs(args); - - Assert.AreEqual("/",result.FirstOrDefault()); - Assert.AreEqual("/test",result[1]); + + Assert.AreEqual("/", result.FirstOrDefault()); + Assert.AreEqual("/test", result[1]); } - + [TestMethod] public void GetPathListFormArgsTest_IgnoreNullOrWhiteSpace() { - var args = new List {"-p", "\"/;\""}.ToArray(); + var args = new List { "-p", "\"/;\"" }.ToArray(); var result = new ArgsHelper(_appSettings).GetPathListFormArgs(args); - + Assert.AreEqual(1, result.Count); - Assert.AreEqual("/",result.FirstOrDefault()); + Assert.AreEqual("/", result.FirstOrDefault()); } - + [TestMethod] public void GetPathListFormArgsTest_CurrentDirectory() { - var args = new List {"-p"}.ToArray(); + var args = new List { "-p" }.ToArray(); var result = new ArgsHelper(_appSettings).GetPathListFormArgs(args); - + Assert.AreEqual(1, result.Count); - Assert.AreEqual(Directory.GetCurrentDirectory(),result.FirstOrDefault()); + Assert.AreEqual(Directory.GetCurrentDirectory(), result.FirstOrDefault()); } [TestMethod] @@ -178,15 +177,15 @@ public void GetPathListFormArgsTest_CurrentDirectory() public void GetPathListFormArgsTest__FieldAccessException() { // inject appSettings! - var args = new List {"-p", "/"}.ToArray(); + var args = new List { "-p", "/" }.ToArray(); new ArgsHelper(null!).GetPathListFormArgs(args); } - + [TestMethod] public void ArgsHelper_GetPath_WithHelp_CurrentDirectory_Test() { var args = new List { "-p", "-h" }.ToArray(); - var value = new ArgsHelper(_appSettings).GetPathFormArgs(args,false); + var value = new ArgsHelper(_appSettings).GetPathFormArgs(args, false); var currentDir = Directory.GetCurrentDirectory(); Assert.AreEqual(currentDir, value); @@ -196,34 +195,34 @@ public void ArgsHelper_GetPath_WithHelp_CurrentDirectory_Test() public void ArgsHelper_GetPath_CurrentDirectory_Test() { var args = new List { "-p" }.ToArray(); - var value = new ArgsHelper(_appSettings).GetPathFormArgs(args,false); + var value = new ArgsHelper(_appSettings).GetPathFormArgs(args, false); Assert.AreEqual(Directory.GetCurrentDirectory(), value); } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_GetSubpathFormArgsTest() { _appSettings.StorageFolder = new CreateAnImage().BasePath; - var args = new List {"-s", "/"}.ToArray(); - Assert.AreEqual("/",ArgsHelper.GetSubPathFormArgs(args)); - } - + var args = new List { "-s", "/" }.ToArray(); + Assert.AreEqual("/", ArgsHelper.GetSubPathFormArgs(args)); + } + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_IfSubPathTest() { _appSettings.StorageFolder = new CreateAnImage().BasePath; - var args = new List {"-s", "/"}.ToArray(); + var args = new List { "-s", "/" }.ToArray(); Assert.IsTrue(ArgsHelper.IsSubPathOrPath(args)); - + // Default - args = new List{string.Empty}.ToArray(); + args = new List { string.Empty }.ToArray(); Assert.IsTrue(ArgsHelper.IsSubPathOrPath(args)); - - args = new List {"-p", "/"}.ToArray(); + + args = new List { "-p", "/" }.ToArray(); Assert.IsFalse(ArgsHelper.IsSubPathOrPath(args)); } @@ -231,7 +230,7 @@ public void ArgsHelper_IfSubPathTest() public void ArgsHelper_CurrentDirectory_IfSubpathTest() { // for selecting the current directory - var args = new List {"-p"}.ToArray(); + var args = new List { "-p" }.ToArray(); Assert.IsFalse(ArgsHelper.IsSubPathOrPath(args)); } @@ -240,55 +239,54 @@ public void ArgsHelper_CurrentDirectory_IfSubpathTest() public void ArgsHelper_GetThumbnailTest() { _appSettings.StorageFolder = new CreateAnImage().BasePath; - var args = new List {"-t", "true"}.ToArray(); + var args = new List { "-t", "true" }.ToArray(); Assert.IsTrue(ArgsHelper.GetThumbnail(args)); - } - + } + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_GetOrphanFolderCheckTest() { _appSettings.StorageFolder = new CreateAnImage().BasePath; - var args = new List {"-o", "true"}.ToArray(); + var args = new List { "-o", "true" }.ToArray(); Assert.IsTrue(new ArgsHelper(_appSettings).GetOrphanFolderCheck(args)); - } - + } + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_GetMoveTest() { - var args = new List {"-m"}.ToArray(); + var args = new List { "-m" }.ToArray(); Assert.IsTrue(ArgsHelper.GetMove(args)); - + // Bool parse check - args = new List {"-m","true"}.ToArray(); + args = new List { "-m", "true" }.ToArray(); Assert.IsTrue(ArgsHelper.GetMove(args)); } - + [TestMethod] public void ArgsHelper_GetMoveTest2() { // Bool parse check - var args = new List {"-m","false"}.ToArray(); + var args = new List { "-m", "false" }.ToArray(); Assert.IsFalse(ArgsHelper.GetMove(args)); } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_GetAllTest() { - var args = new List {"-a"}.ToArray(); + var args = new List { "-a" }.ToArray(); Assert.AreEqual(true, ArgsHelper.GetAll(args)); - + // Bool parse check - args = new List {"-a","false"}.ToArray(); + args = new List { "-a", "false" }.ToArray(); Assert.AreEqual(false, ArgsHelper.GetAll(args)); - + args = new List().ToArray(); Assert.AreEqual(false, ArgsHelper.GetAll(args)); - } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_SetEnvironmentByArgsShortTestListTest() @@ -297,72 +295,70 @@ public void ArgsHelper_SetEnvironmentByArgsShortTestListTest() var envNameList = new ArgsHelper(_appSettings).EnvNameList.ToArray(); var shortTestList = new List(); - for (int i = 0; i < shortNameList.Length; i++) + for ( int i = 0; i < shortNameList.Length; i++ ) { shortTestList.Add(shortNameList[i]); shortTestList.Add(i.ToString()); } - + new ArgsHelper(_appSettings).SetEnvironmentByArgs(shortTestList); - - for (int i = 0; i < envNameList.Length; i++) + + for ( int i = 0; i < envNameList.Length; i++ ) { - Assert.AreEqual(Environment.GetEnvironmentVariable(envNameList[i]),i.ToString()); + Assert.AreEqual(Environment.GetEnvironmentVariable(envNameList[i]), i.ToString()); } - + // Reset Environment after use - foreach (var t in envNameList) + foreach ( var t in envNameList ) { - Environment.SetEnvironmentVariable(t,string.Empty); + Environment.SetEnvironmentVariable(t, string.Empty); } - } - + [TestMethod] [ExcludeFromCoverage] public void ArgsHelper_SetEnvironmentByArgsLongTestListTest() { var longNameList = new ArgsHelper(_appSettings).LongNameList.ToArray(); var envNameList = new ArgsHelper(_appSettings).EnvNameList.ToArray(); - + var longTestList = new List(); - for (int i = 0; i < longNameList.Length; i++) + for ( int i = 0; i < longNameList.Length; i++ ) { longTestList.Add(longNameList[i]); longTestList.Add(i.ToString()); } - + new ArgsHelper(_appSettings).SetEnvironmentByArgs(longTestList); - for (int i = 0; i < envNameList.Length; i++) + for ( int i = 0; i < envNameList.Length; i++ ) { - Assert.AreEqual(Environment.GetEnvironmentVariable(envNameList[i]),i.ToString()); + Assert.AreEqual(Environment.GetEnvironmentVariable(envNameList[i]), i.ToString()); } - + // Reset Environment after use - foreach (var t in envNameList) + foreach ( var t in envNameList ) { - Environment.SetEnvironmentVariable(t,string.Empty); + Environment.SetEnvironmentVariable(t, string.Empty); } - } [TestMethod] public void ArgsHelper_GetSubPathRelativeTest() { - var args = new List {"--subpathrelative", "1"}.ToArray(); + var args = new List { "--subpathrelative", "1" }.ToArray(); var relative = new ArgsHelper(_appSettings).GetRelativeValue(args); Assert.AreEqual(-1, relative); } - + [TestMethod] public void ArgsHelper_GetSubPathRelativeTestMinusValue() { - var args = new List {"--subpathrelative", "-1"}.ToArray(); + var args = new List { "--subpathrelative", "-1" }.ToArray(); var relative = new ArgsHelper(_appSettings).GetRelativeValue(args); Assert.AreEqual(-1, relative); } - + [TestMethod] [ExpectedException(typeof(FieldAccessException))] public void ArgsHelper_GetSubPathRelative_Null_Test() @@ -374,49 +370,53 @@ public void ArgsHelper_GetSubPathRelative_Null_Test() [TestMethod] public void ArgsHelper_GetSubPathRelativeTestLargeInt() { - var args = new List {"--subpathrelative", "201801020"}.ToArray(); + var args = new List { "--subpathrelative", "201801020" }.ToArray(); var relative = new ArgsHelper(_appSettings).GetRelativeValue(args); Assert.IsNull(relative); } - + [TestMethod] public void ArgsHelper_NeedHelpShowDialog_Thumbnail() { var console = new FakeConsoleWrapper(); // Just simple show a console dialog - new ArgsHelper(new AppSettings { - ApplicationType = AppSettings.StarskyAppType.Thumbnail, - Verbose = true - },console) + new ArgsHelper( + new AppSettings + { + ApplicationType = AppSettings.StarskyAppType.Thumbnail, Verbose = true + }, console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("Thumbnail")); } - + [TestMethod] public void ArgsHelper_NeedHelpShowDialog_MetaThumbnail() { var console = new FakeConsoleWrapper(); // Just simple show a console dialog - new ArgsHelper(new AppSettings { - ApplicationType = AppSettings.StarskyAppType.MetaThumbnail, - Verbose = true - },console) + new ArgsHelper( + new AppSettings + { + ApplicationType = AppSettings.StarskyAppType.MetaThumbnail, + Verbose = true + }, console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("MetaThumbnail")); } - + [TestMethod] public void ArgsHelper_NeedHelpShowDialog_Admin() { var console = new FakeConsoleWrapper(); - new ArgsHelper(new AppSettings { - ApplicationType = AppSettings.StarskyAppType.Admin, - Verbose = true - },console) + new ArgsHelper( + new AppSettings + { + ApplicationType = AppSettings.StarskyAppType.Admin, Verbose = true + }, console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("Admin")); } - + [TestMethod] public void ArgsHelper_NeedHelpShowDialog_Geo() { @@ -425,21 +425,22 @@ public void ArgsHelper_NeedHelpShowDialog_Geo() { ApplicationType = AppSettings.StarskyAppType.Geo }; - new ArgsHelper(geoAppSettings,console) + new ArgsHelper(geoAppSettings, console) .NeedHelpShowDialog(); - + Assert.IsNotNull(geoAppSettings); Assert.IsTrue(console.WrittenLines[0].Contains("Geo")); } - + [TestMethod] public void ArgsHelper_NeedHelpShowDialog_WebHtml() { var console = new FakeConsoleWrapper(); - new ArgsHelper(new AppSettings { - ApplicationType = AppSettings.StarskyAppType.WebHtml, - Verbose = true - },console) + new ArgsHelper( + new AppSettings + { + ApplicationType = AppSettings.StarskyAppType.WebHtml, Verbose = true + }, console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("WebHtml")); } @@ -448,7 +449,9 @@ public void ArgsHelper_NeedHelpShowDialog_WebHtml() public void ArgsHelper_NeedHelpShowDialog_Importer() { var console = new FakeConsoleWrapper(); - new ArgsHelper(new AppSettings {ApplicationType = AppSettings.StarskyAppType.Importer},console) + new ArgsHelper( + new AppSettings { ApplicationType = AppSettings.StarskyAppType.Importer }, + console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("Importer")); } @@ -457,7 +460,8 @@ public void ArgsHelper_NeedHelpShowDialog_Importer() public void ArgsHelper_NeedHelpShowDialog_Sync() { var console = new FakeConsoleWrapper(); - new ArgsHelper(new AppSettings {ApplicationType = AppSettings.StarskyAppType.Sync},console) + new ArgsHelper(new AppSettings { ApplicationType = AppSettings.StarskyAppType.Sync }, + console) .NeedHelpShowDialog(); Assert.IsTrue(console.WrittenLines[0].Contains("Sync")); } @@ -467,33 +471,35 @@ public void NeedHelpShowDialog_WebHtml_Verbose() { var consoleWrapper = new FakeConsoleWrapper(); var appSettings = - new AppSettings { - Verbose = true, - ApplicationType = AppSettings.StarskyAppType.WebHtml, - PublishProfiles = new Dictionary>{ + new AppSettings + { + Verbose = true, + ApplicationType = AppSettings.StarskyAppType.WebHtml, + PublishProfiles = new Dictionary> { - "_d", new List { - new AppSettingsPublishProfiles + "_d", + new List { - Append = "_append", - Copy = true, - Folder = "folder" + new AppSettingsPublishProfiles + { + Append = "_append", Copy = true, Folder = "folder" + } } } - }} + } }; - - new ArgsHelper(appSettings, consoleWrapper ) + + new ArgsHelper(appSettings, consoleWrapper) .NeedHelpShowDialog(); var contains = consoleWrapper.WrittenLines.Contains( "--- Path: Append: _append Copy: True Folder: folder/ Prepend: Template: " + "ContentType: None MetaData: True OverlayMaxWidth: 100 SourceMaxWidth: 100 "); - + Assert.IsTrue(contains); } - + [TestMethod] [ExpectedException(typeof(FieldAccessException))] public void ArgsHelper_NeedHelpShowDialog_Null_Test() @@ -501,7 +507,7 @@ public void ArgsHelper_NeedHelpShowDialog_Null_Test() new ArgsHelper(null!).NeedHelpShowDialog(); // FieldAccessException } - + [TestMethod] [ExpectedException(typeof(FieldAccessException))] @@ -515,43 +521,44 @@ public void ArgsHelper_SetEnvironmentToAppSettings_Null_Test() public void ArgsHelper_SetEnvironmentToAppSettingsTest() { var appSettings = new AppSettings(); - - + + var shortNameList = new ArgsHelper(appSettings).ShortNameList.ToArray(); var envNameList = new ArgsHelper(appSettings).EnvNameList.ToArray(); var shortTestList = new List(); - for (int i = 0; i < envNameList.Length; i++) + for ( int i = 0; i < envNameList.Length; i++ ) { shortTestList.Add(shortNameList[i]); if ( envNameList[i] == "app__DatabaseType" ) { shortTestList.Add("InMemoryDatabase"); // need to exact good - continue; + continue; } if ( envNameList[i] == "app__Structure" ) { shortTestList.Add("/{filename}.ext"); - continue; + continue; } - + if ( envNameList[i] == "app__DatabaseConnection" ) { shortTestList.Add("test"); - continue; + continue; } - + if ( envNameList[i] == "app__ExifToolPath" ) { shortTestList.Add("app__ExifToolPath"); - continue; + continue; } + if ( envNameList[i] == "app__StorageFolder" ) { shortTestList.Add("app__StorageFolder"); - continue; + continue; } Console.WriteLine(envNameList[i]); @@ -567,46 +574,46 @@ public void ArgsHelper_SetEnvironmentToAppSettingsTest() shortTestList.Add(i.ToString()); } - + // First inject values to evn new ArgsHelper(appSettings).SetEnvironmentByArgs(shortTestList); - - + + // and now read it back new ArgsHelper(appSettings).SetEnvironmentToAppSettings(); - Assert.AreEqual("/{filename}.ext",appSettings.Structure); - Assert.AreEqual(AppSettings.DatabaseTypeList.InMemoryDatabase,appSettings.DatabaseType); - Assert.AreEqual("test",appSettings.DatabaseConnection); - Assert.AreEqual("app__ExifToolPath",appSettings.ExifToolPath); - Assert.AreEqual(true,appSettings.StorageFolder.Contains("app__StorageFolder")); + Assert.AreEqual("/{filename}.ext", appSettings.Structure); + Assert.AreEqual(AppSettings.DatabaseTypeList.InMemoryDatabase, + appSettings.DatabaseType); + Assert.AreEqual("test", appSettings.DatabaseConnection); + Assert.AreEqual("app__ExifToolPath", appSettings.ExifToolPath); + Assert.AreEqual(true, appSettings.StorageFolder.Contains("app__StorageFolder")); + - - // Reset Environment after use - foreach (var t in envNameList) + foreach ( var t in envNameList ) { - Environment.SetEnvironmentVariable(t,string.Empty); + Environment.SetEnvironmentVariable(t, string.Empty); } } - + [TestMethod] public void ArgsHelper_GetColorClass() { - var args = new List {"--colorclass", "1"}.ToArray(); + var args = new List { "--colorclass", "1" }.ToArray(); var value = ArgsHelper.GetColorClass(args); Assert.AreEqual(1, value); } - + [TestMethod] public void ArgsHelper_GetColorClass_99_Fallback() { - var args = new List {"--colorclass", "99"}.ToArray(); + var args = new List { "--colorclass", "99" }.ToArray(); var value = ArgsHelper.GetColorClass(args); Assert.AreEqual(-1, value); } - + [TestMethod] public void ArgsHelper_GetColorClassFallback() { @@ -614,13 +621,13 @@ public void ArgsHelper_GetColorClassFallback() var value = ArgsHelper.GetColorClass(args); Assert.AreEqual(-1, value); } - + [TestMethod] public void Name() { _appSettings.StorageFolder = new CreateAnImage().BasePath; - var args = new List {"-n", "test"}.ToArray(); - Assert.AreEqual("test",ArgsHelper.GetName(args)); - } + var args = new List { "-n", "test" }.ToArray(); + Assert.AreEqual("test", ArgsHelper.GetName(args)); + } } } diff --git a/starsky/starskytest/Helpers/Base64HelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/Base64HelperTest.cs similarity index 94% rename from starsky/starskytest/Helpers/Base64HelperTest.cs rename to starsky/starskytest/starsky.foundation.platform/Helpers/Base64HelperTest.cs index 904039511b..da848ed78e 100644 --- a/starsky/starskytest/Helpers/Base64HelperTest.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/Base64HelperTest.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.platform.Helpers; -namespace starskytest.Helpers +namespace starskytest.starsky.foundation.platform.Helpers { [TestClass] public sealed class Base64HelperTest @@ -39,6 +39,5 @@ public void Base64HelperTest_TryParseCorruptString() var noByte = Array.Empty(); Assert.AreEqual(noByte.Length, currupt.Length); } - } } diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/Compare/AreListsEqualHelperTests.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/Compare/AreListsEqualHelperTests.cs new file mode 100644 index 0000000000..ce2ba985cb --- /dev/null +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/Compare/AreListsEqualHelperTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Helpers.Compare; + +namespace starskytest.starsky.foundation.platform.Helpers.Compare; + +[TestClass] +public class AreListsEqualHelperTests +{ + [TestMethod] + public void AreListsEqual_SameLists_ReturnsTrue() + { + // Arrange + var list1 = new List { 1, 2, 3 }; + var list2 = new List { 1, 2, 3 }; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + public void AreListsEqual_DifferentLists_ReturnsFalse() + { + // Arrange + var list1 = new List { 1, 2, 3 }; + var list2 = new List { 1, 2, 4 }; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void AreListsEqual_DifferentCountLists_ReturnsFalse() + { + // Arrange + var list1 = new List { 1, 2, 3, 4 }; + var list2 = new List { 1, 4 }; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AreListsEqual_NullLists_ArgumentNullException() + { + // Arrange + List list1 = null!; + List list2 = null!; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AreListsEqual_OneListNull_ArgumentNullException() + { + // Arrange + var list1 = new List { 1, 2, 3 }; + List? list2 = null; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2!); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + public void AreListsEqual_OneListNull() + { + // Arrange + var list1 = new List { 1, null, 3 }; + var list2 = new List { 1, 2, 3 }; + + // Act + var result = AreListsEqualHelper.AreListsEqual(list1, list2); + + // Assert // not sure if good + Assert.IsTrue(result); + } +} diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/EnumHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/EnumHelperTest.cs deleted file mode 100644 index 5cd6cd8547..0000000000 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/EnumHelperTest.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Helpers; - -namespace starskytest.starsky.foundation.platform.Helpers; - -[TestClass] -public class EnumHelperTest -{ - public enum TestValue - { - [Display(Name = "Test One")] - Value1, - - [Display(Name = "Test Two")] - Value2, - - Value3, - - [Display(Name = null)] - Value4 - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsDisplayName_ForEnumWithDisplayName() - { - // Arrange - var enumValue = TestValue.Value1; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual("Test One", result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsEnumValue_ForEnumWithoutDisplayName() - { - // Arrange - var enumValue = TestValue.Value3; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual(null, result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsEmptyString_ForEnumWithNullDisplayName() - { - // Arrange - var enumValue = TestValue.Value4; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual(null, result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsEmptyString_ForNullEnum() - { - // Arrange - TestValue? enumValue = null; - - // Act - var result = EnumHelper.GetDisplayName(enumValue!); - - // Assert - Assert.AreEqual(null, result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsDisplayName_ForNullableEnumWithDisplayName() - { - // Arrange - TestValue? enumValue = TestValue.Value1; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual("Test One", result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsEnumValue_ForNullableEnumWithoutDisplayName() - { - // Arrange - TestValue? enumValue = TestValue.Value3; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual(null, result); - } - - [TestMethod] - public void Test_GetDisplayName_ReturnsEmptyString_ForNullableEnumWithNullDisplayName() - { - // Arrange - TestValue? enumValue = TestValue.Value4; - - // Act - var result = EnumHelper.GetDisplayName(enumValue); - - // Assert - Assert.AreEqual(null, result); - } - -} diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelper2Test.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelper2Test.cs index ffa2f4b750..fe00ba6da3 100644 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelper2Test.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelper2Test.cs @@ -1,19 +1,19 @@ using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.platform.Helpers; -using starskycore.Attributes; +using starsky.project.web.Attributes; namespace starskytest.starsky.foundation.platform.Helpers { [TestClass] public sealed class PathHelper2Test { - [ExcludeFromCoverage] [TestMethod] public void ConfigRead_RemoveLatestBackslashTest() { - var input = PathHelper.RemoveLatestBackslash("/2018"+ Path.DirectorySeparatorChar.ToString()); + var input = + PathHelper.RemoveLatestBackslash("/2018" + Path.DirectorySeparatorChar.ToString()); var output = "/2018"; Assert.AreEqual(input, output); } @@ -26,7 +26,7 @@ public void ConfigRead_PrefixDbslashTest() var output = "/2018/"; Assert.AreEqual(input, output); } - + [ExcludeFromCoverage] [TestMethod] public void ConfigRead_AddBackslashTest() @@ -34,9 +34,9 @@ public void ConfigRead_AddBackslashTest() var input = PathHelper.AddBackslash("2018"); var output = "2018" + Path.DirectorySeparatorChar.ToString(); Assert.AreEqual(input, output); - + input = PathHelper.AddBackslash("2018" + Path.DirectorySeparatorChar.ToString()); - output = "2018"+ Path.DirectorySeparatorChar.ToString(); + output = "2018" + Path.DirectorySeparatorChar.ToString(); Assert.AreEqual(input, output); } @@ -44,8 +44,7 @@ public void ConfigRead_AddBackslashTest() [TestMethod] public void ConfigRead_RemovePrefixDbSlashTest() { - Assert.AreEqual("2018",PathHelper.RemovePrefixDbSlash("/2018")); + Assert.AreEqual("2018", PathHelper.RemovePrefixDbSlash("/2018")); } - } } diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs index 4f3143822d..11f62d3c5d 100644 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/PathHelperTest.cs @@ -39,7 +39,7 @@ public async Task GetFileName_ReturnsFileName_WithMaliciousInput_UnixOnly() new MemoryStream(CreateAnImageA6600.Bytes.ToArray())); var result = string.Empty; - for ( var i = 0; i < 100; i++ ) + for ( var i = 0; i < 200; i++ ) { result += test + test2 + test + test; } diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/PortProgramHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/PortProgramHelperTest.cs deleted file mode 100644 index 51ee3536ee..0000000000 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/PortProgramHelperTest.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Helpers; -using starsky.foundation.platform.Models; -using starsky.foundation.storage.Helpers; -using starsky.foundation.storage.Storage; - -namespace starskytest.starsky.foundation.platform.Helpers; - -[TestClass] -public class PortProgramHelperTest -{ - private readonly string? _prePort; - private readonly string? _preAspNetUrls; - - public PortProgramHelperTest() - { - _prePort = Environment.GetEnvironmentVariable("PORT"); - _preAspNetUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); - } - - [TestMethod] - public void SetEnvPortAspNetUrls_ShouldSet() - { - Environment.SetEnvironmentVariable("PORT","8000"); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - PortProgramHelper.SetEnvPortAspNetUrls(new List()); - - Assert.AreEqual("http://*:8000",Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - } - - [TestMethod] - public async Task SetEnvPortAspNetUrlsAndSetDefault_ShouldSet() - { - Environment.SetEnvironmentVariable("PORT","8000"); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(Array.Empty(),string.Empty); - Assert.AreEqual("http://*:8000",Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - } - - [TestMethod] - public void SetEnvPortAspNetUrls_ShouldIgnore() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - PortProgramHelper.SetEnvPortAspNetUrls(new List()); - Assert.AreEqual(null,Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - } - - [TestMethod] - public async Task SetEnvPortAspNetUrlsAndSetDefault_ShouldIgnore_DueAppSettingsFile1() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - var appSettingsPath = Path.Combine(new AppSettings().BaseDirectoryProject,"appsettings-222.json"); - var stream = StringToStreamHelper.StringToStream("{ \"Kestrel\": {\n \"Endpoints\": {\n " + - " \"Https\": {\n \"Url\": \"https://*:8001\"\n },\n \"Http\": {\n " + - " \"Url\": \"http://*:8000\"\n }\n }\n }\n }"); - await new StorageHostFullPathFilesystem().WriteStreamAsync(stream,appSettingsPath); - - await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(Array.Empty(),appSettingsPath); - - Assert.AreEqual(null,Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - - // remove afterwards - new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); - } - - - [TestMethod] - public async Task SkipForAppSettingsJsonFile_ShouldIgnore_DueAppSettingsFile() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - var appSettingsPath = Path.Combine(new AppSettings().BaseDirectoryProject,"appsettings-111.json"); - var stream = StringToStreamHelper.StringToStream("{ \"Kestrel\": {\n \"Endpoints\": {\n " + - " \"Https\": {\n \"Url\": \"https://*:8001\"\n },\n \"Http\": {\n " + - " \"Url\": \"http://*:8000\"\n }\n }\n }\n }"); - await new StorageHostFullPathFilesystem().WriteStreamAsync(stream,appSettingsPath); - - var result = await PortProgramHelper.SkipForAppSettingsJsonFile(appSettingsPath); - - Assert.AreEqual(true,result); - Assert.AreEqual(null,Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - - // remove afterwards - new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); - } - - [TestMethod] - public async Task SkipForAppSettingsJsonFile_ShouldIgnore_DueAppSettingsFile2() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - var appSettingsPath = Path.Combine(new AppSettings().BaseDirectoryProject,"appsettings-333.json"); - var stream = StringToStreamHelper.StringToStream("{ \"Kestrel\": {\n \"Endpoints\": {\n " + - " \"Https\": {\n \"Url\": \"https://*:8001\"\n }\n " + - "\n }\n }\n }"); - await new StorageHostFullPathFilesystem().WriteStreamAsync(stream,appSettingsPath); - - var result = await PortProgramHelper.SkipForAppSettingsJsonFile(appSettingsPath); - - Assert.AreEqual(true,result); - Assert.AreEqual(null,Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - - // remove afterwards - new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); - } - - - [TestMethod] - public async Task SkipForAppSettingsJsonFile_ShouldFalse() - { - var result = await PortProgramHelper.SkipForAppSettingsJsonFile(string.Empty); - Assert.AreEqual(false,result); - } - - [TestMethod] - public void SetDefaultAspNetCoreUrls_ShouldSet() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",""); - - PortProgramHelper.SetDefaultAspNetCoreUrls(Array.Empty()); - - // should set to default - Assert.AreEqual("http://localhost:4000;https://localhost:4001", - Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - } - - [TestMethod] - public void SetDefaultAspNetCoreUrls_ShouldIgnore() - { - Environment.SetEnvironmentVariable("PORT",""); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS","http://localhost:4000"); - - PortProgramHelper.SetDefaultAspNetCoreUrls(Array.Empty()); - - // should set port to 4000 - Assert.AreEqual("http://localhost:4000",Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); - - Environment.SetEnvironmentVariable("PORT",_prePort); - Environment.SetEnvironmentVariable("ASPNETCORE_URLS",_preAspNetUrls); - } -} diff --git a/starsky/starskytest/starsky.foundation.platform/JsonConverter/EnumListConverterTest.cs b/starsky/starskytest/starsky.foundation.platform/JsonConverter/EnumListConverterTest.cs new file mode 100644 index 0000000000..635cca1b95 --- /dev/null +++ b/starsky/starskytest/starsky.foundation.platform/JsonConverter/EnumListConverterTest.cs @@ -0,0 +1,159 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using starsky.foundation.platform.JsonConverter; + +namespace starskytest.starsky.foundation.platform.JsonConverter; + +[TestClass] +public class EnumListConverterTests +{ + [TestMethod] + [ExpectedException(typeof(JsonException))] + public void No_StartArray() + { + // Arrange + const string json = "{\"ValueTypes\":\"Value1\"}"; + var options = DefaultJsonSerializer.CamelCase; + JsonSerializer.Deserialize(json, options); + } + + [TestMethod] + public void TestYourEnumContainer_Deserialize() + { + // Arrange + const string json = "{\"ValueTypes\":[\"Value1\",\"Value2\",\"Value3\"]}"; + var options = DefaultJsonSerializer.CamelCase; + + // Act + var container = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.IsNotNull(container); + Assert.IsNotNull(container.ValueTypes); + Assert.AreEqual(3, container.ValueTypes.Count); + Assert.AreEqual(ValueType.Value1, container.ValueTypes[0]); + Assert.AreEqual(ValueType.Value2, container.ValueTypes[1]); + Assert.AreEqual(ValueType.Value3, container.ValueTypes[2]); + } + + [TestMethod] + public void TestYourEnumContainer_Serialize() + { + // Arrange + var container = new ValueTypeContainer + { + ValueTypes = [ValueType.Value1, ValueType.Value2] + }; + + // Act + var json = JsonSerializer.Serialize(container, DefaultJsonSerializer.CamelCaseNoEnters); + const string expectedJson = "{\"valueTypes\":[\"Value1\",\"Value2\"]}"; + + // Assert + Assert.AreEqual(expectedJson, json); + } + + [TestMethod] + [ExpectedException(typeof(JsonException), "Unknown enum value: InvalidValue")] + public void TestYourEnumContainer_Deserialize_InvalidValue() + { + // Arrange + const string json = "{\"ValueTypes\":[\"Value1\",\"InvalidValue\",\"Value2\"]}"; + + // Act + JsonSerializer.Deserialize(json, DefaultJsonSerializer.CamelCase); + + // Assert + // Should throw JsonException + } + + [TestMethod] + [ExpectedException(typeof(JsonException), "Unexpected end of JSON input")] + public void TestYourEnumContainer_Deserialize_UnexpectedEnd() + { + // Arrange + const string json = "{\"ValueTypes\":[\"Value1\",\"Value2\""; + + // Act + JsonSerializer.Deserialize(json, DefaultJsonSerializer.CamelCase); + + // Assert + // Should throw JsonException + } + + [TestMethod] + [ExpectedException(typeof(JsonException))] + public void Read_WhenTokenTypeIsNotStartArray_ThrowsJsonException() + { + // Arrange + var reader = new Utf8JsonReader(Array.Empty()); + var converter = + new EnumListConverter(); // Replace YourEnum with the actual enum type + + // Act & Assert + converter.Read(ref reader, typeof(List), new JsonSerializerOptions()); + } + + [TestMethod] + [ExpectedException(typeof(JsonException))] + public void Read_WhenTokenTypeIsNotString_ThrowsJsonException() + { + // Arrange + var reader = new Utf8JsonReader(new[] { ( byte )'[', ( byte )'1', ( byte )']' }); + var converter = new EnumListConverter(); + + // Act & Assert + converter.Read(ref reader, typeof(List), new JsonSerializerOptions()); + } + + [TestMethod] + public void Read_ValidJsonArrayWithEnum() + { + // Arrange + var converter = new EnumListConverter(); + + const string json = "[\"Value1\", \"Value2\"]"; + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + + // Act & Assert + var result = + converter.Read(ref reader, typeof(List), new JsonSerializerOptions()); + + CollectionAssert.AreEqual(new List { ValueType.Value1, ValueType.Value2 }, + result); + } + + [TestMethod] + [ExpectedException(typeof(JsonException))] + public void InvalidArrayWithNumber_ThrowJsonException() + { + // Arrange + var converter = new EnumListConverter(); + + // JSON array with a non-string token + const string json = "[1]"; + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + + // Act & Assert + converter.Read(ref reader, typeof(List), new JsonSerializerOptions()); + } + + public class ValueTypeContainer + { + [JsonConverter(typeof(EnumListConverter))] + public List ValueTypes { get; set; } = []; + } + + public enum ValueType + { + Value1, + Value2, + Value3 + } +} diff --git a/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplicationTest.cs b/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplicationTest.cs new file mode 100644 index 0000000000..e836bd8351 --- /dev/null +++ b/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsDefaultEditorApplicationTest.cs @@ -0,0 +1,47 @@ +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.JsonConverter; +using starsky.foundation.platform.Models; + +namespace starskytest.starsky.foundation.platform.Models; + +[TestClass] +public class AppSettingsDefaultEditorApplicationTest +{ + [TestMethod] + public void Json_CompareOutputOfString() + { + // Create an instance of MyClass + var myClass = new AppSettingsDefaultEditorApplication + { + ImageFormats = + [ExtensionRolesHelper.ImageFormat.bmp], + ApplicationPath = @"C:\Program Files\MyApp\MyApp.exe" + }; + + // Serialize the object to JSON + var json = JsonSerializer.Serialize(myClass, DefaultJsonSerializer.CamelCaseNoEnters); + + const string expected = "{\"imageFormats\":[\"bmp\"]," + + "\"applicationPath\":\"C:\\\\Program Files\\\\MyApp\\\\MyApp.exe\"}"; + Assert.AreEqual(expected, json); + } + + [TestMethod] + public void Json_CompareInputOfString() + { + // Create an instance of MyClass + const string input = "{\"imageFormats\":[\"bmp\"]," + + "\"applicationPath\":\"C:\\\\Program Files\\\\MyApp\\\\MyApp.exe\"}"; + + // Serialize the object to JSON + var json = JsonSerializer.Deserialize(input, + DefaultJsonSerializer.CamelCaseNoEnters); + + Assert.IsNotNull(json); + Assert.AreEqual(@"C:\Program Files\MyApp\MyApp.exe", json.ApplicationPath); + Assert.AreEqual(1, json.ImageFormats.Count); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.bmp, json.ImageFormats[0]); + } +} diff --git a/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsTransferObjectTest.cs b/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsTransferObjectTest.cs new file mode 100644 index 0000000000..222f66acda --- /dev/null +++ b/starsky/starskytest/starsky.foundation.platform/Models/AppSettingsTransferObjectTest.cs @@ -0,0 +1,37 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Models; + +namespace starskytest.starsky.foundation.platform.Models; + +[TestClass] +public class AppSettingsTransferObjectTest +{ + [TestMethod] + public void AppSettingsTransferObject_Verbose() + { + var appSettingsTransferObject = new AppSettingsTransferObject + { + Verbose = true, + StorageFolder = "test", + UseSystemTrash = true, + UseLocalDesktop = true, + DefaultDesktopEditor = + [ + new AppSettingsDefaultEditorApplication + { + ApplicationPath = "app", + ImageFormats = [ExtensionRolesHelper.ImageFormat.bmp] + } + ] + }; + + Assert.AreEqual(true, appSettingsTransferObject.Verbose); + Assert.AreEqual("test", appSettingsTransferObject.StorageFolder); + Assert.AreEqual(true, appSettingsTransferObject.UseSystemTrash); + Assert.AreEqual(true, appSettingsTransferObject.UseLocalDesktop); + Assert.AreEqual("app", appSettingsTransferObject.DefaultDesktopEditor[0].ApplicationPath); + Assert.AreEqual(ExtensionRolesHelper.ImageFormat.bmp, + appSettingsTransferObject.DefaultDesktopEditor[0].ImageFormats[0]); + } +} diff --git a/starsky/starskytest/starsky.foundation.readmeta/Services/ReadMeta_ExifReadTest.cs b/starsky/starskytest/starsky.foundation.readmeta/Services/ReadMeta_ExifReadTest.cs index 116b724fd0..3e2c6ff125 100644 --- a/starsky/starskytest/starsky.foundation.readmeta/Services/ReadMeta_ExifReadTest.cs +++ b/starsky/starskytest/starsky.foundation.readmeta/Services/ReadMeta_ExifReadTest.cs @@ -12,7 +12,7 @@ using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; using starsky.foundation.readmeta.ReadMetaHelpers; -using starskycore.Attributes; +using starsky.project.web.Attributes; using starskytest.FakeCreateAn; using starskytest.FakeMocks; using XmpCore; @@ -39,21 +39,20 @@ public MockDirectory(Dictionary tagNameMap) : base(tagNameMap) [TestClass] public sealed class ExifReadTest { - [TestMethod] public void ExifRead_GetObjectNameNull() { - var t = ReadMetaExif.GetObjectName(new List{new MockDirectory(null!)}); - Assert.AreEqual( string.Empty,t); + var t = ReadMetaExif.GetObjectName(new List { new MockDirectory(null!) }); + Assert.AreEqual(string.Empty, t); } [TestMethod] public void ExifRead_GetObjectNameTest() { var dir = new IptcDirectory(); - dir.Set(IptcDirectory.TagObjectName, "test" ); - var t = ReadMetaExif.GetObjectName(new List{dir}); - Assert.AreEqual("test",t); + dir.Set(IptcDirectory.TagObjectName, "test"); + var t = ReadMetaExif.GetObjectName(new List { dir }); + Assert.AreEqual("test", t); } [TestMethod] @@ -62,29 +61,29 @@ public void ExifRead_GetCaptionAbstractTest() { var dir = new IptcDirectory(); dir.Set(IptcDirectory.TagCaption, "test123"); - var t = ReadMetaExif.GetCaptionAbstract(new List{dir}); + var t = ReadMetaExif.GetCaptionAbstract(new List { dir }); Assert.AreEqual("test123", t); } - + [TestMethod] public void ExifRead_GetExifKeywordsSingleTest() { var dir = new IptcDirectory(); dir.Set(IptcDirectory.TagKeywords, "test123"); - var t = ReadMetaExif.GetExifKeywords(new List{dir}); + var t = ReadMetaExif.GetExifKeywords(new List { dir }); Assert.AreEqual("test123", t); } - + [TestMethod] public void ExifRead_GetExifKeywordsMultipleTest() { var dir = new IptcDirectory(); dir.Set(IptcDirectory.TagKeywords, "test123;test12"); - var t = ReadMetaExif.GetExifKeywords(new List{dir}); - Assert.AreEqual("test123, test12",t); //with space + var t = ReadMetaExif.GetExifKeywords(new List { dir }); + Assert.AreEqual("test123, test12", t); //with space } - + [TestMethod] public void ExifRead_GetExifDateTimeTest() { @@ -96,10 +95,12 @@ public void ExifRead_GetExifDateTimeTest() dir2.Set(ExifDirectoryBase.TagDateTimeOriginal, "2010:12:12 12:41:35"); dir2.Set(ExifDirectoryBase.TagDateTime, "2010:12:12 12:41:35"); container.Add(dir2); - - var result = new ReadMetaExif(null!,null!, new FakeIWebLogger()).GetExifDateTime(container); - var expectedExifDateTime = new DateTime(2010, 12, 12, 12, 41, 35, kind: DateTimeKind.Local); - + + var result = + new ReadMetaExif(null!, null!, new FakeIWebLogger()).GetExifDateTime(container); + var expectedExifDateTime = + new DateTime(2010, 12, 12, 12, 41, 35, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); } @@ -110,65 +111,73 @@ public void ExifRead_GetExifDateTimeTest_TagDateTimeOriginal() var dir2 = new ExifSubIfdDirectory(); dir2.Set(ExifDirectoryBase.TagDateTimeOriginal, "2010:12:12 12:41:35"); container.Add(dir2); - - var result = new ReadMetaExif(null!,null!, new FakeIWebLogger()).GetExifDateTime(container); - var expectedExifDateTime = new DateTime(2010, 12, 12, 12, 41, 35, kind: DateTimeKind.Local); - + + var result = + new ReadMetaExif(null!, null!, new FakeIWebLogger()).GetExifDateTime(container); + var expectedExifDateTime = + new DateTime(2010, 12, 12, 12, 41, 35, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] public void ExifRead_GetExifDateTimeTest_QuickTimeMovieHeaderDirectory_SetUtc() { var orgCulture = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + var container = new List(); var dir2 = new QuickTimeMovieHeaderDirectory(); dir2.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); container.Add(dir2); - - var result = new ReadMetaExif(null!, new AppSettings{ VideoUseLocalTime = new List - { - new CameraMakeModel("test","test") - }, - CameraTimeZone = "Europe/London" - },null!).GetExifDateTime(container, new CameraMakeModel("test","test")); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); - + + var result = + new ReadMetaExif(null!, + new AppSettings + { + VideoUseLocalTime = + new List { new CameraMakeModel("test", "test") }, + CameraTimeZone = "Europe/London" + }, null!).GetExifDateTime(container, new CameraMakeModel("test", "test")); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); - + CultureInfo.CurrentCulture = new CultureInfo(orgCulture); } - + [TestMethod] public void ExifRead_GetExifDateTimeTest_QuickTimeMovieHeaderDirectory_BrandOnly() { var orgCulture = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + var container = new List(); var dir2 = new QuickTimeMovieHeaderDirectory(); dir2.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); container.Add(dir2); - - var result = new ReadMetaExif(null!, new AppSettings{ VideoUseLocalTime = new List - { - new CameraMakeModel("test", string.Empty) - }, - CameraTimeZone = "Europe/London" - }, new FakeIWebLogger()).GetExifDateTime(container, new CameraMakeModel("test","test")); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); - + + var result = new ReadMetaExif(null!, + new AppSettings + { + VideoUseLocalTime = + new List { new CameraMakeModel("test", string.Empty) }, + CameraTimeZone = "Europe/London" + }, new FakeIWebLogger()) + .GetExifDateTime(container, new CameraMakeModel("test", "test")); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); - + CultureInfo.CurrentCulture = new CultureInfo(orgCulture); } - + [TestMethod] [ExcludeFromCoverage] public void ExifRead_GetExifDateTimeTest_QuickTimeMovieHeaderDirectory_AssumeLocal() @@ -176,26 +185,31 @@ public void ExifRead_GetExifDateTimeTest_QuickTimeMovieHeaderDirectory_AssumeLoc var orgCulture = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + var container = new List(); var dir2 = new QuickTimeMovieHeaderDirectory(); dir2.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); container.Add(dir2); - - var result = new ReadMetaExif(null!, new AppSettings{ VideoUseLocalTime = new List + + var result = new ReadMetaExif(null!, + new AppSettings { - new CameraMakeModel("Apple", string.Empty) - }, - CameraTimeZone = "Europe/London" - }, new FakeIWebLogger()).GetExifDateTime(container); - + VideoUseLocalTime = + new List + { + new CameraMakeModel("Apple", string.Empty) + }, + CameraTimeZone = "Europe/London" + }, new FakeIWebLogger()).GetExifDateTime(container); + CultureInfo.CurrentCulture = new CultureInfo(orgCulture); - var expectedExifDateTime = new DateTime(2011, 10, 11, 10, 40, 4, kind: DateTimeKind.Local); - + var expectedExifDateTime = + new DateTime(2011, 10, 11, 10, 40, 4, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] [ExcludeFromCoverage] public void ExifRead_GetExifDateTimeTest_GetXmpData() @@ -208,13 +222,16 @@ public void ExifRead_GetExifDateTimeTest_GetXmpData() throw new NullReferenceException( "ExifRead_GetExifDateTimeTest_GetXmpData xmpMeta Field"); } - - dir2.XmpMeta.SetProperty("http://ns.adobe.com/photoshop/1.0/", "photoshop:DateCreated","2020-03-14T14:00:51" ); + + dir2.XmpMeta.SetProperty("http://ns.adobe.com/photoshop/1.0/", "photoshop:DateCreated", + "2020-03-14T14:00:51"); container.Add(dir2); - - var result = new ReadMetaExif(null!,null!, new FakeIWebLogger()).GetExifDateTime(container); - var expectedExifDateTime = new DateTime(2020, 3, 14, 14, 0, 51, kind: DateTimeKind.Local); - + + var result = + new ReadMetaExif(null!, null!, new FakeIWebLogger()).GetExifDateTime(container); + var expectedExifDateTime = + new DateTime(2020, 3, 14, 14, 0, 51, kind: DateTimeKind.Local); + Assert.AreEqual(expectedExifDateTime, result); } @@ -222,7 +239,7 @@ public void ExifRead_GetExifDateTimeTest_GetXmpData() public void ParseSubIfdDateTime_NotInFirstContainer_TagDateTimeOriginal() { var container = new List(); - + // for raw the first container does not contain dates var dir1 = new ExifSubIfdDirectory(); container.Add(dir1); @@ -233,14 +250,15 @@ public void ParseSubIfdDateTime_NotInFirstContainer_TagDateTimeOriginal() var provider = CultureInfo.InvariantCulture; var result = ReadMetaExif.ParseSubIfdDateTime(container, provider); - Assert.AreEqual(new DateTime(2022,02,02,20,22,02, kind: DateTimeKind.Local),result); + Assert.AreEqual(new DateTime(2022, 02, 02, 20, 22, 02, kind: DateTimeKind.Local), + result); } - + [TestMethod] public void ParseSubIfdDateTime_NotInFirstContainer_TagDateTimeDigitized() { var container = new List(); - + // for raw the first container does not contain dates var dir1 = new ExifSubIfdDirectory(); container.Add(dir1); @@ -251,9 +269,10 @@ public void ParseSubIfdDateTime_NotInFirstContainer_TagDateTimeDigitized() var provider = CultureInfo.InvariantCulture; var result = ReadMetaExif.ParseSubIfdDateTime(container, provider); - Assert.AreEqual(new DateTime(2022,02,02,20,22,02, kind: DateTimeKind.Local),result); + Assert.AreEqual(new DateTime(2022, 02, 02, 20, 22, 02, kind: DateTimeKind.Local), + result); } - + [TestMethod] public void ParseSubIfdDateTime_NonValidDate() { @@ -263,91 +282,100 @@ public void ParseSubIfdDateTime_NonValidDate() dir1.Set(ExifDirectoryBase.TagDateTimeDigitized, "test_not_valid_date"); container.Add(dir1); - + var provider = CultureInfo.InvariantCulture; var result = ReadMetaExif.ParseSubIfdDateTime(container, provider); - Assert.AreEqual(new DateTime(),result); + Assert.AreEqual(new DateTime(), result); } - + [TestMethod] public void ParseSubIfdDateTime_Nothing() { var container = new List(); var dir1 = new ExifSubIfdDirectory(); container.Add(dir1); - + var provider = CultureInfo.InvariantCulture; var result = ReadMetaExif.ParseSubIfdDateTime(container, provider); - Assert.AreEqual(new DateTime(),result); + Assert.AreEqual(new DateTime(), result); } [TestMethod] public void ExifRead_ReadExifFromFileTest() { var newImage = CreateAnImage.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); - + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.jpg"); + Assert.AreEqual(ColorClassParser.Color.None, item.ColorClass); - Assert.AreEqual("caption", item.Description ); - Assert.AreEqual(false,item.IsDirectory ); + Assert.AreEqual("caption", item.Description); + Assert.AreEqual(false, item.IsDirectory); Assert.AreEqual("test, sion", item.Tags); Assert.AreEqual("title", item.Title); Assert.AreEqual(52.308205555500003, item.Latitude, 0.000001); - Assert.AreEqual(6.1935555554999997, item.Longitude, 0.000001); + Assert.AreEqual(6.1935555554999997, item.Longitude, 0.000001); Assert.AreEqual(2, item.ImageHeight); - Assert.AreEqual(3,item.ImageWidth); + Assert.AreEqual(3, item.ImageWidth); Assert.AreEqual("Diepenveen", item.LocationCity); - Assert.AreEqual( "Overijssel", item.LocationState); - Assert.AreEqual( "Nederland",item.LocationCountry); - Assert.AreEqual( 6,item.LocationAltitude); + Assert.AreEqual("Overijssel", item.LocationState); + Assert.AreEqual("Nederland", item.LocationCountry); + Assert.AreEqual(6, item.LocationAltitude); Assert.AreEqual(100, item.FocalLength); - Assert.AreEqual(new DateTime(2018,04,22,16,14,54, kind: DateTimeKind.Local), item.DateTime); - - Assert.AreEqual( "Sony|SLT-A58|24-105mm F3.5-4.5", item.MakeModel); - Assert.AreEqual( "Sony", item.Make); - Assert.AreEqual( "SLT-A58", item.Model); - Assert.AreEqual( "24-105mm F3.5-4.5", item.LensModel); - Assert.AreEqual( ImageStabilisationType.Unknown, item.ImageStabilisation); + Assert.AreEqual(new DateTime(2018, 04, 22, 16, 14, 54, kind: DateTimeKind.Local), + item.DateTime); + + Assert.AreEqual("Sony|SLT-A58|24-105mm F3.5-4.5", item.MakeModel); + Assert.AreEqual("Sony", item.Make); + Assert.AreEqual("SLT-A58", item.Model); + Assert.AreEqual("24-105mm F3.5-4.5", item.LensModel); + Assert.AreEqual(ImageStabilisationType.Unknown, item.ImageStabilisation); } - + [TestMethod] public void ImageStabilisationOn() { var newImage = CreateAnImageA6600.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); - Assert.AreEqual( ImageStabilisationType.On, item.ImageStabilisation); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.jpg"); + Assert.AreEqual(ImageStabilisationType.On, item.ImageStabilisation); } - + [TestMethod] public void ImageStabilisationOff() { var newImage = CreateAnImageA58Tamron.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); - Assert.AreEqual( ImageStabilisationType.Off, item.ImageStabilisation); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.jpg"); + Assert.AreEqual(ImageStabilisationType.Off, item.ImageStabilisation); } - + [TestMethod] public void LocationCountryCode() { var newImage = CreateAnImageA6600.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,new AppSettings{Verbose = true}, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); - Assert.AreEqual( "NLD", item.LocationCountryCode); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, new AppSettings { Verbose = true }, + new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); + Assert.AreEqual("NLD", item.LocationCountryCode); } - + [TestMethod] public void LocationCountryCode_ListDir() { @@ -359,41 +387,45 @@ public void LocationCountryCode_ListDir() " NLD\n" + " \n\n" + "\n"; - + var xmpMeta = XmpMetaFactory.ParseFromString(xmpData); var properties = xmpMeta.Properties.Any(); Assert.IsTrue(properties); - + var container = new List(); var dir2 = new XmpDirectory(); dir2.SetXmpMeta(xmpMeta); container.Add(dir2); - var result = ReadMetaExif.GetLocationCountryCode(container); - - Assert.AreEqual( "NLD", result); + var result = ReadMetaExif.GetLocationCountryCode(container); + + Assert.AreEqual("NLD", result); } - + [TestMethod] public void LensModelTamRon() { var newImage = CreateAnImageA58Tamron.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); - Assert.AreEqual( "Tamron or Sigma Lens", item.LensModel); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.jpg"); + Assert.AreEqual("Tamron or Sigma Lens", item.LensModel); } [TestMethod] public void ExifRead_ReadExifFromFileTest_DeletedTag() { var newImage = CreateAnImageStatusDeleted.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.jpg"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.jpg"); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.jpg"); Assert.AreEqual(TrashKeyword.TrashKeywordString, item.Tags); } @@ -401,202 +433,224 @@ public void ExifRead_ReadExifFromFileTest_DeletedTag() public void ExifRead_ReadExif_FromPngInFileXMP_FileTest() { var newImage = CreateAnPng.Bytes.ToArray(); - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.png"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.png"); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.png" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.png"); Assert.AreEqual(ColorClassParser.Color.SuperiorAlt, item.ColorClass); - Assert.AreEqual("Description", item.Description ); - Assert.AreEqual(false,item.IsDirectory ); + Assert.AreEqual("Description", item.Description); + Assert.AreEqual(false, item.IsDirectory); Assert.AreEqual("tags", item.Tags); Assert.AreEqual("title", item.Title); Assert.AreEqual(35.0379999999, item.Latitude, 0.000001); - Assert.AreEqual(-81.0520000001, item.Longitude, 0.000001); + Assert.AreEqual(-81.0520000001, item.Longitude, 0.000001); Assert.AreEqual(1, item.ImageHeight); - Assert.AreEqual(1,item.ImageWidth); + Assert.AreEqual(1, item.ImageWidth); Assert.AreEqual("City", item.LocationCity); - Assert.AreEqual( "State", item.LocationState); - Assert.AreEqual( "Country",item.LocationCountry); - Assert.AreEqual( 10,item.LocationAltitude); + Assert.AreEqual("State", item.LocationState); + Assert.AreEqual("Country", item.LocationCountry); + Assert.AreEqual(10, item.LocationAltitude); Assert.AreEqual(80, item.FocalLength); - Assert.AreEqual(new DateTime(2022,06,12,10,45,31, kind: DateTimeKind.Local), item.DateTime); + Assert.AreEqual(new DateTime(2022, 06, 12, 10, 45, 31, kind: DateTimeKind.Local), + item.DateTime); } - + [TestMethod] public void ExifRead_GetImageWidthHeight_returnNothing() { - var directory = new List {BuildDirectory(new List())}; - var returnNothing = ReadMetaExif.GetImageWidthHeight(directory,true); - Assert.AreEqual(0,returnNothing); - - var returnNothingFalse = ReadMetaExif.GetImageWidthHeight(directory,false); - Assert.AreEqual(0,returnNothingFalse); + var directory = new List { BuildDirectory(new List()) }; + var returnNothing = ReadMetaExif.GetImageWidthHeight(directory, true); + Assert.AreEqual(0, returnNothing); + + var returnNothingFalse = ReadMetaExif.GetImageWidthHeight(directory, false); + Assert.AreEqual(0, returnNothingFalse); } [TestMethod] public void ExifRead_ReadExif_FromQuickTimeMp4InFileXMP_FileTest() { var newImage = CreateAnQuickTimeMp4.Bytes; - var fakeStorage = new FakeIStorage(new List {"/"}, - new List {"/test.mp4"}, new List {newImage.ToArray()}); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.mp4" }, new List { newImage.ToArray() }); - var item = new ReadMetaExif(fakeStorage, new AppSettings{VideoUseLocalTime = new List - { - new CameraMakeModel("Apple","MacbookPro15,1") - }}, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); + var item = new ReadMetaExif(fakeStorage, + new AppSettings + { + VideoUseLocalTime = new List + { + new CameraMakeModel("Apple", "MacbookPro15,1") + } + }, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); var date = new DateTime(2020, 03, 29, 13, 10, 07, kind: DateTimeKind.Local); Assert.AreEqual(date, item.DateTime); Assert.AreEqual(20, item.ImageWidth); Assert.AreEqual(20, item.ImageHeight); - Assert.AreEqual(false,item.IsDirectory ); + Assert.AreEqual(false, item.IsDirectory); } - + [TestMethod] public void ExifRead_ReadExif_FromQuickTimeMp4InFileXMP_FileTest_DutchCulture() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("NL-nl"); - + var newImage = CreateAnQuickTimeMp4.Bytes; - var fakeStorage = new FakeIStorage(new List {"/"}, - new List {"/test.mp4"}, new List {newImage.ToArray()}); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.mp4" }, new List { newImage.ToArray() }); - var item = new ReadMetaExif(fakeStorage, new AppSettings{VideoUseLocalTime = new List - { - new CameraMakeModel("Apple","MacbookPro15,1") - }}, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); + var item = new ReadMetaExif(fakeStorage, + new AppSettings + { + VideoUseLocalTime = new List + { + new CameraMakeModel("Apple", "MacbookPro15,1") + } + }, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); var date = new DateTime(2020, 03, 29, 13, 10, 07, kind: DateTimeKind.Local); Assert.AreEqual(date, item.DateTime); Assert.AreEqual(20, item.ImageWidth); Assert.AreEqual(20, item.ImageHeight); - Assert.AreEqual(false,item.IsDirectory ); - + Assert.AreEqual(false, item.IsDirectory); + CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); } [TestMethod] public void ExifRead_ParseQuickTimeDateTime_AssumeUtc_CameraTimeZoneMissing() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + // CameraTimeZone = "Europe/London" is missing var fakeStorage = new FakeIStorage(); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()); + + var item = new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()); var dir = new QuickTimeMovieHeaderDirectory(); - dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011" ); - + dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); + var result = item.ParseQuickTimeDateTime(new CameraMakeModel(), - new List{dir}); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); + new List { dir }); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] public void ExifRead_ParseQuickTimeDateTime_UseLocalTime() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + var fakeStorage = new FakeIStorage(); - var item = new ReadMetaExif(fakeStorage, new AppSettings - { - VideoUseLocalTime = new List{new CameraMakeModel("test","test")}, - CameraTimeZone = "Europe/London" - }, new FakeIWebLogger()); + var item = new ReadMetaExif(fakeStorage, + new AppSettings + { + VideoUseLocalTime = + new List { new CameraMakeModel("test", "test") }, + CameraTimeZone = "Europe/London" + }, new FakeIWebLogger()); var dir = new QuickTimeMovieHeaderDirectory(); - dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011" ); - - var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test","test"), - new List{dir}); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); + dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); + + var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test", "test"), + new List { dir }); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Local); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] public void ExifRead_ParseQuickTimeDateTime_UseLocalTime1_WithTimeZone() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); - + var fakeStorage = new FakeIStorage(); - var item = new ReadMetaExif(fakeStorage, new AppSettings - { - VideoUseLocalTime = new List(), - CameraTimeZone = "Europe/London" - }, new FakeIWebLogger()); + var item = new ReadMetaExif(fakeStorage, + new AppSettings + { + VideoUseLocalTime = new List(), + CameraTimeZone = "Europe/London" + }, new FakeIWebLogger()); var dir = new QuickTimeMovieHeaderDirectory(); - dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011" ); - - var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test","test"), - new List{dir}); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 10, 40, 4, kind: DateTimeKind.Local); + dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); + + var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test", "test"), + new List { dir }); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 10, 40, 4, kind: DateTimeKind.Local); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] public void ExifRead_ParseQuickTimeDateTime_UseLocalTime_WithTimeZone_Wrong() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); var fakeStorage = new FakeIStorage(); - var item = new ReadMetaExif(fakeStorage, new AppSettings - { - VideoUseLocalTime = new List(), - CameraTimeZone = "" - }, new FakeIWebLogger()); + var item = new ReadMetaExif(fakeStorage, + new AppSettings + { + VideoUseLocalTime = new List(), CameraTimeZone = "" + }, new FakeIWebLogger()); var dir = new QuickTimeMovieHeaderDirectory(); - dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011" ); - - var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test","test"), - new List{dir}); - - var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Utc).ToLocalTime(); + dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); + + var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test", "test"), + new List { dir }); + + var expectedExifDateTime = + new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Utc).ToLocalTime(); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); Assert.AreEqual(expectedExifDateTime, result); } - + [TestMethod] public void ExifRead_ParseQuickTimeDateTime_NoVideoUsedSet() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("EN-us"); var fakeStorage = new FakeIStorage(); - var item = new ReadMetaExif(fakeStorage, new AppSettings - { - VideoUseLocalTime = null, - CameraTimeZone = "" - }, new FakeIWebLogger()); + var item = new ReadMetaExif(fakeStorage, + new AppSettings { VideoUseLocalTime = null, CameraTimeZone = "" }, + new FakeIWebLogger()); var dir = new QuickTimeMovieHeaderDirectory(); - dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011" ); - - var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test","test"), - new List{dir}); - - var expectedExifDateTime = new DateTime(2011, 10, 11, + dir.Set(QuickTimeMovieHeaderDirectory.TagCreated, "Tue Oct 11 09:40:04 2011"); + + var result = item.ParseQuickTimeDateTime(new CameraMakeModel("test", "test"), + new List { dir }); + + var expectedExifDateTime = new DateTime(2011, 10, 11, 9, 40, 4, kind: DateTimeKind.Utc).ToLocalTime(); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); @@ -606,57 +660,64 @@ public void ExifRead_ParseQuickTimeDateTime_NoVideoUsedSet() [TestMethod] public void ExifRead_ReadExif_FromQuickTimeMp4InFileXMP_WithLocation_FileTest_DutchCulture() { - var currentCultureThreeLetterIsoLanguageName = CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; + var currentCultureThreeLetterIsoLanguageName = + CultureInfo.CurrentCulture.ThreeLetterISOLanguageName; CultureInfo.CurrentCulture = new CultureInfo("NL-nl"); - + var newImage = CreateAnQuickTimeMp4.BytesWithLocation.ToArray(); - var fakeStorage = new FakeIStorage(new List {"/"}, - new List {"/test.mp4"}, new List {newImage}); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.mp4" }, new List { newImage }); - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.mp4"); var date = new DateTime(2020, 04, 04, 12, 50, 19, DateTimeKind.Local).ToLocalTime(); Assert.AreEqual(date, item.DateTime); Assert.AreEqual(640, item.ImageWidth); Assert.AreEqual(360, item.ImageHeight); - - Assert.AreEqual(52.23829861111111, item.Latitude,0.001); - Assert.AreEqual(6.025800238715278, item.Longitude,0.001); - Assert.AreEqual(false,item.IsDirectory ); + Assert.AreEqual(52.23829861111111, item.Latitude, 0.001); + Assert.AreEqual(6.025800238715278, item.Longitude, 0.001); + + Assert.AreEqual(false, item.IsDirectory); CultureInfo.CurrentCulture = new CultureInfo(currentCultureThreeLetterIsoLanguageName); } - + [TestMethod] public void ExifRead_ReadExif_FromQuickTimeMp4InFileXMP_WithLocation_FileTest() { var newImage = CreateAnQuickTimeMp4.BytesWithLocation.ToArray(); - var fakeStorage = new FakeIStorage(new List {"/"}, - new List {"/test.mp4"}, new List {newImage}); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.mp4" }, new List { newImage }); - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.mp4"); + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.mp4"); var date = new DateTime(2020, 04, 04, 12, 50, 19, DateTimeKind.Local).ToLocalTime(); Assert.AreEqual(date, item.DateTime); Assert.AreEqual(640, item.ImageWidth); Assert.AreEqual(360, item.ImageHeight); - - Assert.AreEqual(52.23829861111111, item.Latitude,0.001); - Assert.AreEqual(6.025800238715278, item.Longitude,0.001); - Assert.AreEqual(false,item.IsDirectory ); + Assert.AreEqual(52.23829861111111, item.Latitude, 0.001); + Assert.AreEqual(6.025800238715278, item.Longitude, 0.001); + + Assert.AreEqual(false, item.IsDirectory); } [TestMethod] public void ExifRead_DataParsingCorruptFailsData() { var newImage = CreateAnPng.Bytes.Take(200).ToArray(); // corrupt - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.png"},new List{newImage}); - - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.png"); - + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.png" }, new List { newImage }); + + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.png"); + Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, item.Status); Assert.AreEqual(ExtensionRolesHelper.ImageFormat.unknown, item.ImageFormat); } @@ -664,11 +725,13 @@ public void ExifRead_DataParsingCorruptFailsData() [TestMethod] public void ExifRead_DataParsingCorruptStreamNull() { - var fakeStorage = new FakeIStorage(new List{"/"}, - new List{"/test.png"},new List{null!}); - var item = new ReadMetaExif(fakeStorage,null!, new FakeIWebLogger()).ReadExifFromFile("/test.png"); + var fakeStorage = new FakeIStorage(new List { "/" }, + new List { "/test.png" }, new List { null! }); + var item = + new ReadMetaExif(fakeStorage, null!, new FakeIWebLogger()).ReadExifFromFile( + "/test.png"); // streamNull - + Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, item.Status); Assert.AreEqual(FileIndexItem.ExifStatus.OperationNotSupported, item.Status); } @@ -682,12 +745,12 @@ private static MockDirectory BuildDirectory(IEnumerable values) { var directory = new MockDirectory(null!); - foreach (var pair in Enumerable.Range(1, int.MaxValue).Zip(values, Tuple.Create)) + foreach ( var pair in Enumerable.Range(1, int.MaxValue).Zip(values, Tuple.Create) ) directory.Set(pair.Item1, pair.Item2); return directory; } - + // ExifDirectoryBase.TagOrientation // "Top, left side (Horizontal / normal)", -- 1 // "Top, right side (Mirror horizontal)", -- 2 @@ -701,122 +764,121 @@ private static MockDirectory BuildDirectory(IEnumerable values) [TestMethod] public void GetOrientationFromExifItem_1() { - var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 1); // "Top, left side (Horizontal / normal)", -- 1 - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,rotation); + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_2() { - var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 2); - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); // 2 = unsuppored yet // "Top, right side (Mirror horizontal)", - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,rotation); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_3() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 3); // "Bottom, right side (Rotate 180)" - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Rotate180,rotation); + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Rotate180, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_4() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 4); - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); // Bottom, left side (Mirror vertical) - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,rotation); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_5() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 5); // "Left side, top (Mirror horizontal and rotate 270 CW)", - - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,rotation); + + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_6() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 6); // "Right side, top (Rotate 90 CW)", --6 - - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw,rotation); + + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Rotate90Cw, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_7() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 7); // "Right side, bottom (Mirror horizontal and rotate 90 CW)", --7 - - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Horizontal,rotation); + + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Horizontal, rotation); } - + [TestMethod] public void GetOrientationFromExifItem_8() { var dir3 = new ExifIfd0Directory(); dir3.Set(ExifDirectoryBase.TagOrientation, 8); // "Left side, bottom (Rotate 270 CW)") --8 - - var rotation = ReadMetaExif.GetOrientationFromExifItem(new List{dir3}); - Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw,rotation); + + var rotation = ReadMetaExif.GetOrientationFromExifItem(new List { dir3 }); + Assert.AreEqual(FileIndexItem.Rotation.Rotate270Cw, rotation); } - + [TestMethod] public void TestGetXmpGeoData() { // Arrange - - const string xmpData = "\n" + - "\n\n " + - "\n" + - " ALB\n \n\n" + - " \n" + - " 800796/527\n" + - " 0\n" + - " 42,27.7005N\n" + - " 19,52.86888E\n" + - " \n\n \n" + - " Valbone\n" + - " Shqipëri\n" + - " 2023-06-29T11:21:36\n" + - " Kukes County\n " + - "\n\n\n\n"; - + + const string xmpData = + "\n" + + "\n\n " + + "\n" + + " ALB\n \n\n" + + " \n" + + " 800796/527\n" + + " 0\n" + + " 42,27.7005N\n" + + " 19,52.86888E\n" + + " \n\n \n" + + " Valbone\n" + + " Shqipëri\n" + + " 2023-06-29T11:21:36\n" + + " Kukes County\n " + + "\n\n\n\n"; + var xmpMeta = XmpMetaFactory.ParseFromString(xmpData); - + var container = new List(); var dir2 = new XmpDirectory(); dir2.SetXmpMeta(xmpMeta); container.Add(dir2); - + const string propertyPath = "exif:GPSLatitude"; // Act @@ -825,36 +887,37 @@ public void TestGetXmpGeoData() // Assert Assert.AreEqual(42.461675, result, 0.0000000001); } - + [TestMethod] public void TestGetXmpGeoData2() { // Arrange - - const string xmpData = "\n" + - "\n\n " + - "\n" + - " ALB\n \n\n" + - " \n" + - " 800796/527\n" + - " 0\n" + - " 42,27.7005N\n" + - " 19,52.86888E\n" + - " \n\n \n" + - " Valbone\n" + - " Shqipëri\n" + - " 2023-06-29T11:21:36\n" + - " Kukes County\n " + - "\n\n\n\n"; - + + const string xmpData = + "\n" + + "\n\n " + + "\n" + + " ALB\n \n\n" + + " \n" + + " 800796/527\n" + + " 0\n" + + " 42,27.7005N\n" + + " 19,52.86888E\n" + + " \n\n \n" + + " Valbone\n" + + " Shqipëri\n" + + " 2023-06-29T11:21:36\n" + + " Kukes County\n " + + "\n\n\n\n"; + var xmpMeta = XmpMetaFactory.ParseFromString(xmpData); - + var container = new List(); var dir2 = new XmpDirectory(); dir2.SetXmpMeta(xmpMeta); container.Add(dir2); - + const string propertyPath = "exif:GPSLongitude"; // Act diff --git a/starsky/starskytest/starsky.foundation.search/ViewModels/SearchViewModelTest.cs b/starsky/starskytest/starsky.foundation.search/ViewModels/SearchViewModelTest.cs index f7eaeb669a..8d96dd0a46 100644 --- a/starsky/starskytest/starsky.foundation.search/ViewModels/SearchViewModelTest.cs +++ b/starsky/starskytest/starsky.foundation.search/ViewModels/SearchViewModelTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,26 +13,26 @@ public sealed class SearchViewModelTest [TestMethod] public void SearchViewModelTest_TestIfTrash() { - var test = new SearchViewModel {SearchQuery = TrashKeyword.TrashKeywordString}; - Assert.AreEqual(PageViewType.PageType.Trash.ToString(),test.PageType); + var test = new SearchViewModel { SearchQuery = TrashKeyword.TrashKeywordString }; + Assert.AreEqual(PageViewType.PageType.Trash.ToString(), test.PageType); } - + [TestMethod] public void SearchViewModelTest_Default() { var test = new SearchViewModel(); - Assert.AreEqual(PageViewType.PageType.Search.ToString(),test.PageType); + Assert.AreEqual(PageViewType.PageType.Search.ToString(), test.PageType); } [TestMethod] public void SetAddSearchInStringType_Null() { var model = new SearchViewModel(); - - model.GetType().GetProperty(nameof(model.SearchIn))?.SetValue(model, null,null); - + + model.GetType().GetProperty(nameof(model.SearchIn))?.SetValue(model, null, null); + model.SetAddSearchInStringType(null!); - + Assert.AreNotEqual(null, model.SearchIn); } @@ -42,16 +43,17 @@ public void SetAddSearch_searchForType_Null() Assert.AreNotEqual(null, model.SearchFor); } - + [TestMethod] public void SetAddSearch_searchForType_SetAddSearchFor_Null() { var model = new SearchViewModel { SearchForInternal = null }; - model.GetType().GetProperty(nameof(model.SearchForInternal))?.SetValue(model, null,null); + model.GetType().GetProperty(nameof(model.SearchForInternal)) + ?.SetValue(model, null, null); model.SetAddSearchFor(""); - + Assert.AreNotEqual(null, model.SearchFor); } @@ -62,31 +64,34 @@ public void SearchForOptions_Null() Assert.AreNotEqual(null, model.SearchForOptions); } - + [TestMethod] public void SearchForOptions_SetAddSearchForOptions_Null() { var model = new SearchViewModel { SearchForOptionsInternal = null }; - model.GetType().GetProperty(nameof(model.SearchForOptionsInternal))?.SetValue(model, null,null); + model.GetType().GetProperty(nameof(model.SearchForOptionsInternal)) + ?.SetValue(model, null, null); model.SetAddSearchForOptions("test"); - + Assert.AreNotEqual(null, model.SearchForOptions); } - + [TestMethod] public void SearchForOptions_SetAddSearchForOptions_DotComma() { var model = new SearchViewModel { SearchForOptionsInternal = null }; - model.GetType().GetProperty(nameof(model.SearchForOptionsInternal))?.SetValue(model, null,null); + model.GetType().GetProperty(nameof(model.SearchForOptionsInternal)) + ?.SetValue(model, null, null); model.SetAddSearchForOptions(";"); - - Assert.AreEqual(SearchViewModel.SearchForOptionType.Equal, model.SearchForOptions.LastOrDefault()); + + Assert.AreEqual(SearchViewModel.SearchForOptionType.Equal, + model.SearchForOptions.LastOrDefault()); } - + [TestMethod] public void SetAndOrOperator_amp_False() { @@ -99,44 +104,294 @@ public void SetAndOrOperator_amp_False() [TestMethod] public void SearchOperatorContinue_IgnoreNegativeValue() { + var model = new SearchViewModel { SearchOperatorOptionsInternal = new List() }; + var result = model.SearchOperatorContinue(-1, 1); + + Assert.IsTrue(result); + } + + [TestMethod] + public void SearchOperatorContinue_IgnoreOutOfRange() + { + var model = new SearchViewModel { SearchOperatorOptionsInternal = new List() }; + var result = model.SearchOperatorContinue(10, 1); + + Assert.IsTrue(result); + } + + [TestMethod] + public void SearchOperatorContinue_IgnoreOutOfRange2() + { + var model = new SearchViewModel { SearchOperatorOptionsInternal = new List() }; + var result = model.SearchOperatorContinue(0, 1); + + Assert.IsTrue(result); + } + + [TestMethod] + public void NarrowSearch_NoFileIndexItems() + { + var result = SearchViewModel.NarrowSearch(new SearchViewModel()); + Assert.AreEqual(0, result.SearchCount); + } + + [TestMethod] + public void SearchViewModel_ElapsedSeconds_Test() + { + var searchViewModel = new SearchViewModel { ElapsedSeconds = 0.0006 }; + Assert.AreEqual(true, searchViewModel.ElapsedSeconds <= 0.001); + } + + [TestMethod] + public void SearchViewModel_Offset_Test() + { + var searchViewModel = new SearchViewModel(); + Assert.AreEqual(0, Math.Floor(searchViewModel.Offset)); + } + + [TestMethod] + public void PropertySearchTest() + { + var property = new FileIndexItem { Tags = "q" }.GetType() + .GetProperty(nameof(FileIndexItem.Tags))!; + + // not a great test + var search = SearchViewModel.PropertySearch(new SearchViewModel { SearchFor = { "q" } }, + property, + "q", SearchViewModel.SearchForOptionType.Equal); + + Assert.AreEqual(0, search.CollectionsCount); + } + + [TestMethod] + public void PropertySearchStringType_DefaultCase_NullConditions1() + { + // Arrange + var model = new SearchViewModel { FileIndexItems = null }; + + var property = typeof(FileIndexItem).GetProperty("NotFound"); + Assert.IsNull(property); + + const string searchForQuery = "file"; + var searchType = SearchViewModel.SearchForOptionType.Equal; + + // Act + var result = + SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, + searchType); + + // Assert + Assert.IsNotNull(result); + Assert.IsNull(result.FileIndexItems); + } + + [TestMethod] + public void PropertySearchStringType_DefaultCase_NullConditions2() + { + // Arrange + var model = new SearchViewModel { FileIndexItems = null }; + + var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.Tags)); + const string searchForQuery = "file"; + var searchType = SearchViewModel.SearchForOptionType.Equal; + + // Act + var result = + SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, + searchType); + + // Assert + Assert.IsNotNull(result); + Assert.IsNull(result.FileIndexItems); + } + + [TestMethod] + public void PropertySearchStringType_DefaultCase_Found_Null() + { + // Arrange var model = new SearchViewModel { - SearchOperatorOptionsInternal = new List() + FileIndexItems = new List + { + new FileIndexItem("test") { LocationCity = null } + } }; - var result = model.SearchOperatorContinue(-1,1); - - Assert.IsTrue(result); + + var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.LocationCity)); + const string searchForQuery = "file"; + var searchType = SearchViewModel.SearchForOptionType.Equal; + + // Act + var result = + SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, + searchType); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(0, result.FileIndexItems?.Count); } [TestMethod] - public void SearchOperatorContinue_IgnoreOutOfRange() + public void PropertySearchStringType_DefaultCase_Found_HappyFlow() { + // Arrange var model = new SearchViewModel { - SearchOperatorOptionsInternal = new List() + FileIndexItems = new List + { + new FileIndexItem("test") { LocationCity = "test" } + } }; - var result = model.SearchOperatorContinue(10,1); - - Assert.IsTrue(result); + + var property = typeof(FileIndexItem).GetProperty(nameof(FileIndexItem.LocationCity)); + const string searchForQuery = "test"; + var searchType = SearchViewModel.SearchForOptionType.Equal; + + // Act + var result = + SearchViewModel.PropertySearchStringType(model, property!, searchForQuery, + searchType); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(1, result.FileIndexItems?.Count); } - + [TestMethod] - public void SearchOperatorContinue_IgnoreOutOfRange2() + public void PropertySearchBoolType_FiltersItemsByBoolProperty() + { + // Arrange + var model = new SearchViewModel(); + model.FileIndexItems = new List + { + new FileIndexItem { IsDirectory = true }, + new FileIndexItem { IsDirectory = false }, + new FileIndexItem { IsDirectory = true }, + }; + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + var boolIsValue = true; + + // Act + var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); + + // Assert + Assert.AreEqual(2, result.FileIndexItems?.Count); + Assert.IsTrue(result.FileIndexItems?.Exists(item => item.IsDirectory == true)); + } + + [TestMethod] + public void PropertySearchBoolType_WithNullModel_ReturnsNullModel() + { + // Arrange + SearchViewModel? model = null; + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + const bool boolIsValue = true; + + // Act + var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); + + // Assert + Assert.IsNotNull(result); + } + + [TestMethod] + public void PropertySearchBoolType_WithNullFileIndexItems_ReturnsNullFileIndexItems() + { + // Arrange + var model = new SearchViewModel { FileIndexItems = null, }; + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + var boolIsValue = true; + + // Act + var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); + + // Assert + Assert.IsNull(result.FileIndexItems); + } + + [TestMethod] + public void PropertySearchBoolType_WithEmptyFileIndexItems_ReturnsEmptyFileIndexItems() + { + // Arrange + var model = new SearchViewModel { FileIndexItems = new List(), }; + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + var boolIsValue = true; + + // Act + var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); + + // Assert + Assert.IsNotNull(result.FileIndexItems); + Assert.AreEqual(0, result.FileIndexItems.Count); + } + + [TestMethod] + public void PropertySearchBoolType_WithInvalidProperty_ReturnsOriginalModel() { + // Arrange + var model = new SearchViewModel(); + model.FileIndexItems = new List + { + new FileIndexItem { IsDirectory = true }, + }; + var property = typeof(FileIndexItem).GetProperty("NonExistentProperty"); + var boolIsValue = true; + + // Act + var result = SearchViewModel.PropertySearchBoolType(model, property, boolIsValue); + + // Assert + Assert.AreEqual(model, result); + } + + [TestMethod] + public void PropertySearch_WithBoolPropertyAndValidBoolValue_ReturnsFilteredModel() + { + // Arrange var model = new SearchViewModel { - SearchOperatorOptionsInternal = new List() + FileIndexItems = new List + { + new FileIndexItem { IsDirectory = true }, + new FileIndexItem { IsDirectory = false }, + new FileIndexItem { IsDirectory = true }, + } }; - var result = model.SearchOperatorContinue(0,1); - - Assert.IsTrue(result); + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + const string searchForQuery = "true"; + const SearchViewModel.SearchForOptionType searchType = + SearchViewModel.SearchForOptionType.Equal; + + // Act + var result = + SearchViewModel.PropertySearch(model, property!, searchForQuery, searchType); + + // Assert + Assert.AreEqual(2, result.FileIndexItems?.Count); + Assert.IsTrue(result.FileIndexItems?.Exists(item => item.IsDirectory == true)); } [TestMethod] - public void NarrowSearch_NoFileIndexItems() + public void PropertySearch_WithBoolPropertyAndInvalidBoolValue_ReturnsOriginalModel() { - var result = SearchViewModel.NarrowSearch(new SearchViewModel()); - Assert.AreEqual(0, result.SearchCount); + // Arrange + var model = new SearchViewModel(); + model.FileIndexItems = new List + { + new FileIndexItem { IsDirectory = true }, + new FileIndexItem { IsDirectory = false }, + }; + var property = typeof(FileIndexItem).GetProperty("IsDirectory"); + const string searchForQuery = "invalid_bool_value"; // An invalid boolean string + const SearchViewModel.SearchForOptionType searchType = + SearchViewModel.SearchForOptionType.Equal; // You can set this as needed + + // Act + var result = + SearchViewModel.PropertySearch(model, property!, searchForQuery, searchType); + + // Assert + CollectionAssert.AreEqual(model.FileIndexItems, result.FileIndexItems); } } } diff --git a/starsky/starskytest/starsky.foundation.storage/Models/FolderOrFileModelTest.cs b/starsky/starskytest/starsky.foundation.storage/Models/FolderOrFileModelTest.cs new file mode 100644 index 0000000000..ba7fb41a8b --- /dev/null +++ b/starsky/starskytest/starsky.foundation.storage/Models/FolderOrFileModelTest.cs @@ -0,0 +1,18 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.storage.Models; + +namespace starskytest.starsky.foundation.storage.Models +{ + [TestClass] + public sealed class FolderOrFileModelTest + { + [TestMethod] + public void FolderOrFileModelFolderOrFileTypeListTest() + { + const FolderOrFileModel.FolderOrFileTypeList searchType = + FolderOrFileModel.FolderOrFileTypeList.Folder; + var folderOrFileModel = new FolderOrFileModel { IsFolderOrFile = searchType }; + Assert.AreEqual(searchType, folderOrFileModel.IsFolderOrFile); + } + } +} diff --git a/starsky/starskytest/starsky.foundation.writemeta/Helpers/EnumHelperTest.cs b/starsky/starskytest/starsky.foundation.writemeta/Helpers/EnumHelperTest.cs new file mode 100644 index 0000000000..b036f03a3a --- /dev/null +++ b/starsky/starskytest/starsky.foundation.writemeta/Helpers/EnumHelperTest.cs @@ -0,0 +1,168 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.writemeta.Helpers; + +namespace starskytest.starsky.foundation.writemeta.Helpers; + +[TestClass] +public class EnumHelperTest +{ + public enum TestValue + { + [Display(Name = "Test One")] Value1, + + [Display(Name = "Test Two")] Value2, + + Value3, + + [Display(Name = null)] Value4 + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsDisplayName_ForEnumWithDisplayName() + { + // Arrange + var enumValue = TestValue.Value1; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual("Test One", result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsEnumValue_ForEnumWithoutDisplayName() + { + // Arrange + var enumValue = TestValue.Value3; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual(null, result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsEmptyString_ForEnumWithNullDisplayName() + { + // Arrange + var enumValue = TestValue.Value4; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual(null, result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsEmptyString_ForNullEnum() + { + // Arrange + TestValue? enumValue = null; + + // Act + var result = EnumHelper.GetDisplayName(enumValue!); + + // Assert + Assert.AreEqual(null, result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsDisplayName_ForNullableEnumWithDisplayName() + { + // Arrange + TestValue? enumValue = TestValue.Value1; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual("Test One", result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsEnumValue_ForNullableEnumWithoutDisplayName() + { + // Arrange + TestValue? enumValue = TestValue.Value3; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual(null, result); + } + + [TestMethod] + public void Test_GetDisplayName_ReturnsEmptyString_ForNullableEnumWithNullDisplayName() + { + // Arrange + TestValue? enumValue = TestValue.Value4; + + // Act + var result = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual(null, result); + } + + public enum TestType + { + [Display(Name = "First Display Name")] FirstValue, + + [Display(Name = "Second Display Name")] + SecondValue + } + + [TestMethod] + public void GetDisplayName_WithValidEnumValue_ReturnsCorrectDisplayName() + { + // Arrange + var enumValue = TestType.FirstValue; + + // Act + var displayName = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual("First Display Name", displayName); + } + + [TestMethod] + public void GetDisplayName_WithNullEnumValue_ReturnsNull() + { + // Arrange & Act + var displayName = EnumHelper.GetDisplayName(null!); + + // Assert + Assert.IsNull(displayName); + } + + [TestMethod] + public void GetDisplayName_WithInvalidEnumValue_ReturnsNull() + { + // Arrange + var enumValue = ( TestType )100; // An invalid value + + // Act + var displayName = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.IsNull(displayName); + } + + [TestMethod] + public void GetDisplayName_WithEnumValueWithoutDisplayAttribute_ReturnsNull() + { + // Arrange + var enumValue = TestType.SecondValue; + + // Act + var displayName = EnumHelper.GetDisplayName(enumValue); + + // Assert + Assert.AreEqual("Second Display Name", displayName); + } +} diff --git a/starsky/starskytest/starsky.foundation.writemeta/Helpers/ExifToolCmdHelperTest.cs b/starsky/starskytest/starsky.foundation.writemeta/Helpers/ExifToolCmdHelperTest.cs index b1538e93ea..4791852eb2 100644 --- a/starsky/starskytest/starsky.foundation.writemeta/Helpers/ExifToolCmdHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.writemeta/Helpers/ExifToolCmdHelperTest.cs @@ -7,7 +7,6 @@ using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; using starskytest.FakeMocks; -using starskytest.Models; using ExifToolCmdHelper = starsky.foundation.writemeta.Helpers.ExifToolCmdHelper; namespace starskytest.starsky.foundation.writemeta.Helpers @@ -42,7 +41,8 @@ public void ExifToolCmdHelper_UpdateTest() Orientation = FileIndexItem.Rotation.Rotate90Cw, DateTime = DateTime.Now, }; - var comparedNames = new List{ + var comparedNames = new List + { nameof(FileIndexItem.Tags).ToLowerInvariant(), nameof(FileIndexItem.Description).ToLowerInvariant(), nameof(FileIndexItem.Latitude).ToLowerInvariant(), @@ -57,231 +57,240 @@ public void ExifToolCmdHelper_UpdateTest() nameof(FileIndexItem.Orientation).ToLowerInvariant(), nameof(FileIndexItem.DateTime).ToLowerInvariant(), }; - - var inputSubPaths = new List - { - "/test.jpg" - }; - var storage = new FakeIStorage(new List{"/"},new List{"/test.jpg"},new List()); - - var fakeExifTool = new FakeExifTool(storage,_appSettings); - var helperResult = new ExifToolCmdHelper(fakeExifTool, storage,storage , - new FakeReadMeta(), new FakeIThumbnailQuery()).Update(updateModel, inputSubPaths, comparedNames); - - Assert.AreEqual(true,helperResult.Contains(updateModel.Tags)); - Assert.AreEqual(true,helperResult.Contains(updateModel.Description)); - Assert.AreEqual(true,helperResult.Contains(updateModel.Latitude.ToString(CultureInfo.InvariantCulture))); - Assert.AreEqual(true,helperResult.Contains(updateModel.Longitude.ToString(CultureInfo.InvariantCulture))); - Assert.AreEqual(true,helperResult.Contains(updateModel.LocationAltitude.ToString(CultureInfo.InvariantCulture))); - Assert.AreEqual(true,helperResult.Contains(updateModel.LocationCity)); - Assert.AreEqual(true,helperResult.Contains(updateModel.LocationState)); - Assert.AreEqual(true,helperResult.Contains(updateModel.LocationCountry)); - Assert.AreEqual(true,helperResult.Contains(updateModel.LocationCountryCode)); - Assert.AreEqual(true,helperResult.Contains(updateModel.Title)); + + var inputSubPaths = new List { "/test.jpg" }; + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List()); + + var fakeExifTool = new FakeExifTool(storage, _appSettings); + var helperResult = new ExifToolCmdHelper(fakeExifTool, storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .Update(updateModel, inputSubPaths, comparedNames); + + Assert.AreEqual(true, helperResult.Contains(updateModel.Tags)); + Assert.AreEqual(true, helperResult.Contains(updateModel.Description)); + Assert.AreEqual(true, + helperResult.Contains(updateModel.Latitude.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(true, + helperResult.Contains( + updateModel.Longitude.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(true, + helperResult.Contains( + updateModel.LocationAltitude.ToString(CultureInfo.InvariantCulture))); + Assert.AreEqual(true, helperResult.Contains(updateModel.LocationCity)); + Assert.AreEqual(true, helperResult.Contains(updateModel.LocationState)); + Assert.AreEqual(true, helperResult.Contains(updateModel.LocationCountry)); + Assert.AreEqual(true, helperResult.Contains(updateModel.LocationCountryCode)); + Assert.AreEqual(true, helperResult.Contains(updateModel.Title)); } [TestMethod] public void ExifToolCmdHelper_Update_UpdateLocationAltitudeCommandTest() { - var updateModel = new FileIndexItem + var updateModel = new FileIndexItem { LocationAltitude = -41, }; + var comparedNames = new List { - LocationAltitude = -41, - }; - var comparedNames = new List{ nameof(FileIndexItem.LocationAltitude).ToLowerInvariant(), }; - - var folderPaths = new List{"/"}; - var inputSubPaths = new List{"/test.jpg"}; + var folderPaths = new List { "/" }; + + var inputSubPaths = new List { "/test.jpg" }; var storage = new FakeIStorage(folderPaths, inputSubPaths); - var fakeExifTool = new FakeExifTool(storage,_appSettings); + var fakeExifTool = new FakeExifTool(storage, _appSettings); - var helperResult = new ExifToolCmdHelper(fakeExifTool, - storage,storage, - new FakeReadMeta(), new FakeIThumbnailQuery()).Update(updateModel, inputSubPaths, comparedNames); - - Assert.AreEqual(true,helperResult.Contains("-GPSAltitude=\"-41")); - Assert.AreEqual(true,helperResult.Contains("gpsaltituderef#=\"1")); + var helperResult = new ExifToolCmdHelper(fakeExifTool, + storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .Update(updateModel, inputSubPaths, comparedNames); + Assert.AreEqual(true, helperResult.Contains("-GPSAltitude=\"-41")); + Assert.AreEqual(true, helperResult.Contains("gpsaltituderef#=\"1")); } [TestMethod] public async Task CreateXmpFileIsNotExist_NotCreateFile_jpg() { - var updateModel = new FileIndexItem - { - LocationAltitude = -41, - }; - var folderPaths = new List{"/"}; + var updateModel = new FileIndexItem { LocationAltitude = -41, }; + var folderPaths = new List { "/" }; - var inputSubPaths = new List{"/test.jpg"}; + var inputSubPaths = new List { "/test.jpg" }; var storage = new FakeIStorage(folderPaths, inputSubPaths); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - await new ExifToolCmdHelper(fakeExifTool, - storage,storage, - new FakeReadMeta(), new FakeIThumbnailQuery()).CreateXmpFileIsNotExist(updateModel, inputSubPaths); + var fakeExifTool = new FakeExifTool(storage, _appSettings); + await new ExifToolCmdHelper(fakeExifTool, + storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .CreateXmpFileIsNotExist(updateModel, inputSubPaths); Assert.IsFalse(storage.ExistFile("/test.xmp")); } - - [TestMethod] - public async Task CreateXmpFileIsNotExist_CreateFile_dng() - { - var updateModel = new FileIndexItem - { - LocationAltitude = -41, - }; - var folderPaths = new List{"/"}; - - var inputSubPaths = new List{"/test.dng"}; - - var storage = - new FakeIStorage(folderPaths, inputSubPaths); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - await new ExifToolCmdHelper(fakeExifTool, - storage,storage, - new FakeReadMeta(), new FakeIThumbnailQuery()).CreateXmpFileIsNotExist(updateModel, inputSubPaths); - - Assert.IsTrue(storage.ExistFile("/test.xmp")); - } - - [TestMethod] - public async Task UpdateAsync_ShouldUpdate_SkipFileHash() - { - var updateModel = new FileIndexItem + + [TestMethod] + public async Task CreateXmpFileIsNotExist_CreateFile_dng() + { + var updateModel = new FileIndexItem { LocationAltitude = -41, }; + var folderPaths = new List { "/" }; + + var inputSubPaths = new List { "/test.dng" }; + + var storage = + new FakeIStorage(folderPaths, inputSubPaths); + var fakeExifTool = new FakeExifTool(storage, _appSettings); + await new ExifToolCmdHelper(fakeExifTool, + storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .CreateXmpFileIsNotExist(updateModel, inputSubPaths); + + Assert.IsTrue(storage.ExistFile("/test.xmp")); + } + + [TestMethod] + public async Task UpdateAsync_ShouldUpdate_SkipFileHash() + { + var updateModel = new FileIndexItem { Tags = "tags", Description = "Description", }; + var comparedNames = new List + { + nameof(FileIndexItem.Tags).ToLowerInvariant(), + nameof(FileIndexItem.Description).ToLowerInvariant(), + }; + + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List()); + + var fakeExifTool = new FakeExifTool(storage, _appSettings); + var helperResult = ( await new ExifToolCmdHelper(fakeExifTool, storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .UpdateAsync(updateModel, comparedNames) ); + + Assert.IsTrue(helperResult.Item1.Contains("tags")); + Assert.IsTrue(helperResult.Item1.Contains("Description")); + } + + [TestMethod] + public async Task UpdateAsync_ShouldUpdate_IncludeFileHash() + { + var updateModel = new FileIndexItem { Tags = "tags", Description = "Description", + FileHash = "_hash_test" // < - - - - include here }; - var comparedNames = new List{ + var comparedNames = new List + { nameof(FileIndexItem.Tags).ToLowerInvariant(), nameof(FileIndexItem.Description).ToLowerInvariant(), }; - var storage = new FakeIStorage(new List{"/"},new List{"/test.jpg"},new List()); + var storage = new FakeIStorage(new List { "/" }, + new List { "/test.jpg" }, new List()); + + var fakeExifTool = new FakeExifTool(storage, _appSettings); + var helperResult = ( await new ExifToolCmdHelper(fakeExifTool, storage, storage, + new FakeReadMeta(), new FakeIThumbnailQuery()) + .UpdateAsync(updateModel, comparedNames) ); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - var helperResult = (await new ExifToolCmdHelper(fakeExifTool, storage,storage , - new FakeReadMeta(), new FakeIThumbnailQuery()).UpdateAsync(updateModel, comparedNames)); - Assert.IsTrue(helperResult.Item1.Contains("tags")); Assert.IsTrue(helperResult.Item1.Contains("Description")); - } - - [TestMethod] - public async Task UpdateAsync_ShouldUpdate_IncludeFileHash() - { - var updateModel = new FileIndexItem - { - Tags = "tags", - Description = "Description", - FileHash = "_hash_test" // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.Tags).ToLowerInvariant(), - nameof(FileIndexItem.Description).ToLowerInvariant(), - }; - - var storage = new FakeIStorage(new List{"/"},new List{"/test.jpg"},new List()); - - var fakeExifTool = new FakeExifTool(storage,_appSettings); - var helperResult = (await new ExifToolCmdHelper(fakeExifTool, storage,storage , - new FakeReadMeta(), new FakeIThumbnailQuery()).UpdateAsync(updateModel, comparedNames)); - - Assert.IsTrue(helperResult.Item1.Contains("tags")); - Assert.IsTrue(helperResult.Item1.Contains("Description")); - } - - - [TestMethod] - public void ExifToolCommandLineArgsImageStabilisation() - { - var updateModel = new FileIndexItem - { - ImageStabilisation = ImageStabilisationType.On // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant(), - }; - - var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, - comparedNames, true); - - Assert.AreEqual("-json -overwrite_original -ImageStabilization=\"On\"",result); - } - - [TestMethod] - public void ExifToolCommandLineArgsImageStabilisationUnknown() - { - var updateModel = new FileIndexItem - { - ImageStabilisation = ImageStabilisationType.Unknown // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant(), - }; - - var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, - comparedNames, true); - - Assert.AreEqual(string.Empty,result); - } - - [TestMethod] - public void ExifToolCommandLineArgs_LocationCountryCode() - { - var updateModel = new FileIndexItem - { - LocationCountryCode = "NLD" // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.LocationCountryCode).ToLowerInvariant(), - }; - - var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, - comparedNames, true); - - Assert.AreEqual("-json -overwrite_original -Country-PrimaryLocationCode=\"NLD\" -XMP:CountryCode=\"NLD\"",result); - } - - - [TestMethod] - public void UpdateSoftwareCommand_True() - { - var updateModel = new FileIndexItem - { - Software = "Test" // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.Software).ToLowerInvariant(), - }; - - var result = ExifToolCmdHelper.UpdateSoftwareCommand(string.Empty, comparedNames, updateModel, true); - - Assert.AreEqual(" -Software=\"Test\" -CreatorTool=\"Test\" " + - "-HistorySoftwareAgent=\"Test\" -HistoryParameters=\"\" -PMVersion=\"\" ",result); - } - - [TestMethod] - public void UpdateSoftwareCommand_False() - { - var updateModel = new FileIndexItem - { - Software = "Test" // < - - - - include here - }; - var comparedNames = new List{ - nameof(FileIndexItem.Software).ToLowerInvariant(), - }; - - var result = ExifToolCmdHelper.UpdateSoftwareCommand(string.Empty, comparedNames, updateModel, false); - - Assert.AreEqual(" -Software=\"Starsky\" -CreatorTool=\"Starsky\" " + - "-HistorySoftwareAgent=\"Starsky\" -HistoryParameters=\"\" -PMVersion=\"\" ",result); - } + } + + + [TestMethod] + public void ExifToolCommandLineArgsImageStabilisation() + { + var updateModel = new FileIndexItem + { + ImageStabilisation = ImageStabilisationType.On // < - - - - include here + }; + var comparedNames = new List + { + nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant(), + }; + + var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, + comparedNames, true); + + Assert.AreEqual("-json -overwrite_original -ImageStabilization=\"On\"", result); + } + + [TestMethod] + public void ExifToolCommandLineArgsImageStabilisationUnknown() + { + var updateModel = new FileIndexItem + { + ImageStabilisation = ImageStabilisationType.Unknown // < - - - - include here + }; + var comparedNames = new List + { + nameof(FileIndexItem.ImageStabilisation).ToLowerInvariant(), + }; + + var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, + comparedNames, true); + + Assert.AreEqual(string.Empty, result); + } + + [TestMethod] + public void ExifToolCommandLineArgs_LocationCountryCode() + { + var updateModel = new FileIndexItem + { + LocationCountryCode = "NLD" // < - - - - include here + }; + var comparedNames = new List + { + nameof(FileIndexItem.LocationCountryCode).ToLowerInvariant(), + }; + + var result = ExifToolCmdHelper.ExifToolCommandLineArgs(updateModel, + comparedNames, true); + + Assert.AreEqual( + "-json -overwrite_original -Country-PrimaryLocationCode=\"NLD\" -XMP:CountryCode=\"NLD\"", + result); + } + + + [TestMethod] + public void UpdateSoftwareCommand_True() + { + var updateModel = new FileIndexItem + { + Software = "Test" // < - - - - include here + }; + var comparedNames = + new List { nameof(FileIndexItem.Software).ToLowerInvariant(), }; + + var result = + ExifToolCmdHelper.UpdateSoftwareCommand(string.Empty, comparedNames, updateModel, + true); + + Assert.AreEqual(" -Software=\"Test\" -CreatorTool=\"Test\" " + + "-HistorySoftwareAgent=\"Test\" -HistoryParameters=\"\" -PMVersion=\"\" ", + result); + } + + [TestMethod] + public void UpdateSoftwareCommand_False() + { + var updateModel = new FileIndexItem + { + Software = "Test" // < - - - - include here + }; + var comparedNames = + new List { nameof(FileIndexItem.Software).ToLowerInvariant(), }; + + var result = + ExifToolCmdHelper.UpdateSoftwareCommand(string.Empty, comparedNames, updateModel, + false); + + Assert.AreEqual(" -Software=\"Starsky\" -CreatorTool=\"Starsky\" " + + "-HistorySoftwareAgent=\"Starsky\" -HistoryParameters=\"\" -PMVersion=\"\" ", + result); + } } } diff --git a/starsky/starskytest/starsky.foundation.writemeta/Services/ExifCopyTest.cs b/starsky/starskytest/starsky.foundation.writemeta/Services/ExifCopyTest.cs index 7d463d3e54..0ba7c49fcf 100644 --- a/starsky/starskytest/starsky.foundation.writemeta/Services/ExifCopyTest.cs +++ b/starsky/starskytest/starsky.foundation.writemeta/Services/ExifCopyTest.cs @@ -7,7 +7,6 @@ using starsky.foundation.storage.Helpers; using starsky.foundation.writemeta.Services; using starskytest.FakeMocks; -using starskytest.Models; namespace starskytest.starsky.foundation.writemeta.Services { @@ -21,87 +20,89 @@ public ExifCopyTest() // get the service _appSettings = new AppSettings(); } - + [TestMethod] public async Task ExifToolCmdHelper_CopyExifPublish() { - var folderPaths = new List{"/"}; - var inputSubPaths = new List{"/test.jpg"}; + var folderPaths = new List { "/" }; + var inputSubPaths = new List { "/test.jpg" }; var storage = - new FakeIStorage(folderPaths, inputSubPaths, - new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()}); - + new FakeIStorage(folderPaths, inputSubPaths, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); + var fakeReadMeta = new ReadMeta(storage, _appSettings, null, new FakeIWebLogger()); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - var helperResult = await new ExifCopy(storage, storage, fakeExifTool, + var fakeExifTool = new FakeExifTool(storage, _appSettings); + var helperResult = await new ExifCopy(storage, storage, fakeExifTool, fakeReadMeta, new FakeIThumbnailQuery()).CopyExifPublish("/test.jpg", "/test2"); - Assert.AreEqual(true,helperResult.Contains("HistorySoftwareAgent")); + Assert.AreEqual(true, helperResult.Contains("HistorySoftwareAgent")); } [TestMethod] public async Task ExifToolCmdHelper_XmpSync() { - var folderPaths = new List{"/"}; - var inputSubPaths = new List{"/test.dng"}; + var folderPaths = new List { "/" }; + var inputSubPaths = new List { "/test.dng" }; var storage = - new FakeIStorage(folderPaths, inputSubPaths, - new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + new FakeIStorage(folderPaths, inputSubPaths, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); - var fakeReadMeta = new ReadMeta(storage, _appSettings, + var fakeReadMeta = new ReadMeta(storage, _appSettings, null, new FakeIWebLogger()); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - var helperResult = await new ExifCopy(storage, - storage, fakeExifTool, fakeReadMeta, new FakeIThumbnailQuery()).XmpSync("/test.dng"); - Assert.AreEqual("/test.xmp",helperResult); - + var fakeExifTool = new FakeExifTool(storage, _appSettings); + var helperResult = await new ExifCopy(storage, + storage, fakeExifTool, fakeReadMeta, + new FakeIThumbnailQuery()).XmpSync("/test.dng"); + Assert.AreEqual("/test.xmp", helperResult); } [TestMethod] public async Task ExifToolCmdHelper_XmpCreate() { - var folderPaths = new List{"/"}; - var inputSubPaths = new List{"/test.dng"}; + var folderPaths = new List { "/" }; + var inputSubPaths = new List { "/test.dng" }; var storage = - new FakeIStorage(folderPaths, inputSubPaths, - new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()}); + new FakeIStorage(folderPaths, inputSubPaths, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); var fakeReadMeta = new ReadMeta(storage, _appSettings, null, new FakeIWebLogger()); - var fakeExifTool = new FakeExifTool(storage,_appSettings); - + var fakeExifTool = new FakeExifTool(storage, _appSettings); + new ExifCopy(storage, storage, fakeExifTool, fakeReadMeta, new FakeIThumbnailQuery()) .XmpCreate("/test.xmp"); - var result = await StreamToStringHelper.StreamToStringAsync(storage.ReadStream("/test.xmp")); + var result = + await StreamToStringHelper.StreamToStringAsync(storage.ReadStream("/test.xmp")); Assert.AreEqual("\n" + - "\n\n",result); + "\n\n", + result); } [TestMethod] public async Task ExifToolCmdHelper_TestForFakeExifToolInjection() { - var folderPaths = new List{"/"}; - var inputSubPaths = new List{"/test.dng"}; - + var folderPaths = new List { "/" }; + var inputSubPaths = new List { "/test.dng" }; + var storage = - new FakeIStorage(folderPaths, inputSubPaths, - new List{FakeCreateAn.CreateAnImage.Bytes.ToArray()}); - + new FakeIStorage(folderPaths, inputSubPaths, + new List { FakeCreateAn.CreateAnImage.Bytes.ToArray() }); + var readMeta = new ReadMeta(storage, _appSettings, null, new FakeIWebLogger()); - var fakeExifTool = new FakeExifTool(storage,_appSettings); + var fakeExifTool = new FakeExifTool(storage, _appSettings); await new ExifCopy(storage, storage, fakeExifTool, readMeta, new FakeIThumbnailQuery()) .XmpSync("/test.dng"); - - Assert.AreEqual(true,storage.ExistFile("/test.xmp")); + + Assert.AreEqual(true, storage.ExistFile("/test.xmp")); var xmpContentReadStream = storage.ReadStream("/test.xmp"); var xmpContent = await StreamToStringHelper.StreamToStringAsync(xmpContentReadStream); - - // Those values are injected by fakeExifTool - Assert.AreEqual(true,xmpContent.Contains("")); - Assert.AreEqual(true,xmpContent.Contains("test")); + // Those values are injected by fakeExifTool + Assert.AreEqual(true, + xmpContent.Contains( + "")); + Assert.AreEqual(true, xmpContent.Contains("test")); } - } } diff --git a/starsky/starskytest/Helpers/FilenameHelpersTest.cs b/starsky/starskytest/starsky.project.web/Helpers/FilenameHelpersTest.cs similarity index 98% rename from starsky/starskytest/Helpers/FilenameHelpersTest.cs rename to starsky/starskytest/starsky.project.web/Helpers/FilenameHelpersTest.cs index 2d4b3a6a49..15988ff23f 100644 --- a/starsky/starskytest/Helpers/FilenameHelpersTest.cs +++ b/starsky/starskytest/starsky.project.web/Helpers/FilenameHelpersTest.cs @@ -1,7 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.platform.Helpers; -namespace starskytest.Helpers +namespace starskytest.starsky.project.web.Helpers { [TestClass] public sealed class FilenameHelpersTest diff --git a/starsky/starskytest/starsky.project.web/Helpers/MimeHelperTest.cs b/starsky/starskytest/starsky.project.web/Helpers/MimeHelperTest.cs new file mode 100644 index 0000000000..90e6ab03ba --- /dev/null +++ b/starsky/starskytest/starsky.project.web/Helpers/MimeHelperTest.cs @@ -0,0 +1,55 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.project.web.Helpers; + +namespace starskytest.starsky.project.web.Helpers +{ + [TestClass] + public sealed class MimeHelperTest + { + [TestMethod] + public void GetMimeTypeByFileNameTestUnknown() + { + Assert.AreEqual("application/octet-stream", + MimeHelper.GetMimeTypeByFileName("test.unknown")); + } + + [TestMethod] + public void GetMimeTypeByFileNameTestJpg() + { + Assert.AreEqual("image/jpeg", MimeHelper.GetMimeTypeByFileName("test.jpg")); + } + + [TestMethod] + public void GetMimeTypeByFileNameTestJpeg() + { + Assert.AreEqual("image/jpeg", MimeHelper.GetMimeTypeByFileName("test.jpeg")); + } + + [TestMethod] + public void GetMimeTypeByExtensionTest_NoExtension() + { + Assert.AreEqual("application/octet-stream", + MimeHelper.GetMimeTypeByFileName(string.Empty)); + } + + [TestMethod] + public void GetMimeTypeByExtensionTest_Null() + { + Assert.AreEqual("application/octet-stream", + MimeHelper.GetMimeTypeByFileName(null)); + } + + [TestMethod] + public void GetMimeType_NoExtension() + { + Assert.AreEqual("application/octet-stream", MimeHelper.GetMimeType(string.Empty)); + } + + + [TestMethod] + public void GetMimeType_Jpeg() + { + Assert.AreEqual("image/jpeg", MimeHelper.GetMimeType("jpg")); + } + } +} diff --git a/starsky/starskytest/starsky.project.web/Helpers/PortProgramHelperTest.cs b/starsky/starskytest/starsky.project.web/Helpers/PortProgramHelperTest.cs new file mode 100644 index 0000000000..43ba3a89fa --- /dev/null +++ b/starsky/starskytest/starsky.project.web/Helpers/PortProgramHelperTest.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using starsky.foundation.platform.Models; +using starsky.foundation.storage.Helpers; +using starsky.foundation.storage.Storage; +using starsky.project.web.Helpers; + +namespace starskytest.starsky.project.web.Helpers; + +[TestClass] +public class PortProgramHelperTest +{ + private readonly string? _prePort; + private readonly string? _preAspNetUrls; + + public PortProgramHelperTest() + { + _prePort = Environment.GetEnvironmentVariable("PORT"); + _preAspNetUrls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); + } + + [TestMethod] + public void SetEnvPortAspNetUrls_ShouldSet() + { + Environment.SetEnvironmentVariable("PORT", "8000"); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + PortProgramHelper.SetEnvPortAspNetUrls(new List()); + + Assert.AreEqual("http://*:8000", Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + } + + [TestMethod] + public async Task SetEnvPortAspNetUrlsAndSetDefault_ShouldSet() + { + Environment.SetEnvironmentVariable("PORT", "8000"); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(Array.Empty(), + string.Empty); + Assert.AreEqual("http://*:8000", Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + } + + [TestMethod] + public void SetEnvPortAspNetUrls_ShouldIgnore() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + PortProgramHelper.SetEnvPortAspNetUrls(new List()); + Assert.AreEqual(null, Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + } + + [TestMethod] + public async Task SetEnvPortAspNetUrlsAndSetDefault_ShouldIgnore_DueAppSettingsFile1() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + var appSettingsPath = + Path.Combine(new AppSettings().BaseDirectoryProject, "appsettings-222.json"); + var stream = StringToStreamHelper.StringToStream( + "{ \"Kestrel\": {\n \"Endpoints\": {\n " + + " \"Https\": {\n \"Url\": \"https://*:8001\"\n },\n \"Http\": {\n " + + " \"Url\": \"http://*:8000\"\n }\n }\n }\n }"); + await new StorageHostFullPathFilesystem().WriteStreamAsync(stream, appSettingsPath); + + await PortProgramHelper.SetEnvPortAspNetUrlsAndSetDefault(Array.Empty(), + appSettingsPath); + + Assert.AreEqual(null, Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + + // remove afterwards + new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); + } + + + [TestMethod] + public async Task SkipForAppSettingsJsonFile_ShouldIgnore_DueAppSettingsFile() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + var appSettingsPath = + Path.Combine(new AppSettings().BaseDirectoryProject, "appsettings-111.json"); + var stream = StringToStreamHelper.StringToStream( + "{ \"Kestrel\": {\n \"Endpoints\": {\n " + + " \"Https\": {\n \"Url\": \"https://*:8001\"\n },\n \"Http\": {\n " + + " \"Url\": \"http://*:8000\"\n }\n }\n }\n }"); + await new StorageHostFullPathFilesystem().WriteStreamAsync(stream, appSettingsPath); + + var result = await PortProgramHelper.SkipForAppSettingsJsonFile(appSettingsPath); + + Assert.AreEqual(true, result); + Assert.AreEqual(null, Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + + // remove afterwards + new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); + } + + [TestMethod] + public async Task SkipForAppSettingsJsonFile_ShouldIgnore_DueAppSettingsFile2() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + var appSettingsPath = + Path.Combine(new AppSettings().BaseDirectoryProject, "appsettings-333.json"); + var stream = StringToStreamHelper.StringToStream( + "{ \"Kestrel\": {\n \"Endpoints\": {\n " + + " \"Https\": {\n \"Url\": \"https://*:8001\"\n }\n " + + "\n }\n }\n }"); + await new StorageHostFullPathFilesystem().WriteStreamAsync(stream, appSettingsPath); + + var result = await PortProgramHelper.SkipForAppSettingsJsonFile(appSettingsPath); + + Assert.AreEqual(true, result); + Assert.AreEqual(null, Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + + // remove afterwards + new StorageHostFullPathFilesystem().FileDelete(appSettingsPath); + } + + + [TestMethod] + public async Task SkipForAppSettingsJsonFile_ShouldFalse() + { + var result = await PortProgramHelper.SkipForAppSettingsJsonFile(string.Empty); + Assert.AreEqual(false, result); + } + + [TestMethod] + public void SetDefaultAspNetCoreUrls_ShouldSet() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", ""); + + PortProgramHelper.SetDefaultAspNetCoreUrls(Array.Empty()); + + // should set to default + Assert.AreEqual("http://localhost:4000;https://localhost:4001", + Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + } + + [TestMethod] + public void SetDefaultAspNetCoreUrls_ShouldIgnore() + { + Environment.SetEnvironmentVariable("PORT", ""); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "http://localhost:4000"); + + PortProgramHelper.SetDefaultAspNetCoreUrls(Array.Empty()); + + // should set port to 4000 + Assert.AreEqual("http://localhost:4000", + Environment.GetEnvironmentVariable("ASPNETCORE_URLS")); + + Environment.SetEnvironmentVariable("PORT", _prePort); + Environment.SetEnvironmentVariable("ASPNETCORE_URLS", _preAspNetUrls); + } +} diff --git a/starsky/starskytest/starskytest.csproj b/starsky/starskytest/starskytest.csproj index 9d6f1e90b4..2e55dfdfe0 100644 --- a/starsky/starskytest/starskytest.csproj +++ b/starsky/starskytest/starskytest.csproj @@ -20,13 +20,13 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -42,6 +42,7 @@ + @@ -58,7 +59,7 @@ - + @@ -113,6 +114,18 @@ PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + + + PreserveNewest + diff --git a/starskydesktop/.vscode/launch.bak.json b/starskydesktop/.vscode/launch.bak.json deleted file mode 100644 index c8df8623f5..0000000000 --- a/starskydesktop/.vscode/launch.bak.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "name": "vscode-jest-tests", - "request": "launch", - "args": ["test", "--runInBand"], - "cwd": "${workspaceFolder}", - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/react-scripts", - "protocol": "inspector" - } - ] -} diff --git a/starskydesktop/package.json b/starskydesktop/package.json index 7b078061f7..f8bd4e9036 100644 --- a/starskydesktop/package.json +++ b/starskydesktop/package.json @@ -24,7 +24,7 @@ "dist": "npm run prod && electron-builder build --publish never", "build:win": "electron-builder build --win", "build:mac": "electron-builder build --mac", - "build:runtime": "cd ../starsky && pwsh build.ps1 -runtime osx-arm64,osx-x64,win-x64 -ready-to-run", + "build:runtime": "cd ../starsky && pwsh build.ps1 -runtime osx-arm64,osx-x64,win-x64 -ready-to-run -no-tests", "update": "npx --yes npm-check-updates", "update:install": "npx --yes npm-check-updates -u && npm install", "update:yes": "npm run update:install" diff --git a/starskydesktop/src/app/child-process/setup-child-process.ts b/starskydesktop/src/app/child-process/setup-child-process.ts index 131556a1db..b3d24ffcf6 100644 --- a/starskydesktop/src/app/child-process/setup-child-process.ts +++ b/starskydesktop/src/app/child-process/setup-child-process.ts @@ -55,7 +55,7 @@ export async function setupChildProcess() { app__tempFolder: tempFolder, app__appsettingspath: appSettingsPath, app__NoAccountLocalhost: "true", - app__UseLocalDesktopUi: "true", + app__UseLocalDesktop: "true", app__databaseConnection: databaseConnection, app__AccountRegisterDefaultRole: "Administrator", app__Verbose: !isPackaged() ? "true" : "false", diff --git a/starskydesktop/src/app/edit-file/edit-file.ts b/starskydesktop/src/app/edit-file/edit-file.ts index 403b381d76..f0c899d3b7 100644 --- a/starskydesktop/src/app/edit-file/edit-file.ts +++ b/starskydesktop/src/app/edit-file/edit-file.ts @@ -4,15 +4,12 @@ import { GetBaseUrlFromSettings } from "../config/get-base-url-from-settings"; import UrlQuery from "../config/url-query"; import { createErrorWindow } from "../error-window/create-error-window"; import logger from "../logger/logger"; -import { - GetNetRequest, - IGetNetRequestResponse, -} from "../net-request/get-net-request"; +import { GetNetRequest, IGetNetRequestResponse } from "../net-request/get-net-request"; import { createParentFolders } from "./create-parent-folders"; import { downloadBinary } from "./download-binary"; import { downloadXmpFile } from "./download-xmp-file"; import { IsDetailViewResult } from "./is-detail-view-result"; -import { openPath } from "./open-path"; +import { OpenPath } from "./open-path"; function getFilePathFromWindow(fromMainWindow: BrowserWindow): string { const latestPage = fromMainWindow.webContents.getURL(); @@ -23,7 +20,7 @@ function getFilePathFromWindow(fromMainWindow: BrowserWindow): string { async function openWindow(filePathOnDisk: string) { try { - await openPath(filePathOnDisk); + await OpenPath(filePathOnDisk); } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises createErrorWindow(error as string); @@ -56,9 +53,6 @@ export async function EditFile(fromMainWindow: BrowserWindow) { await createParentFolders(fileIndexItem.parentDirectory); await downloadXmpFile(fileIndexItem, fromMainWindow.webContents.session); - const filePathOnDisk = await downloadBinary( - fileIndexItem, - fromMainWindow.webContents.session - ); + const filePathOnDisk = await downloadBinary(fileIndexItem, fromMainWindow.webContents.session); await openWindow(filePathOnDisk); } diff --git a/starskydesktop/src/app/edit-file/open-path.ts b/starskydesktop/src/app/edit-file/open-path.ts index d4ccbecdad..40e7a2cb37 100644 --- a/starskydesktop/src/app/edit-file/open-path.ts +++ b/starskydesktop/src/app/edit-file/open-path.ts @@ -1,74 +1,12 @@ -import * as childProcess from "child_process"; import { shell } from "electron"; -import * as appConfig from "electron-settings"; -import DefaultImageApplicationSetting from "../config/default-image-application-settings"; import logger from "../logger/logger"; -import OsBuildKey from "../os-info/os-build-key"; -import { IsApplicationRunning } from "./is-application-running"; -/** - * @see: https://community.adobe.com/t5/photoshop/problems-opening-photoshop-open-a/m-p/11541937?page=1 - * Since nobody cares - */ -async function ShouldRunFirst() { - return IsApplicationRunning(".app/Contents/MacOS/Adobe\\ Photoshop"); -} - -function openWindows( - overWriteDefaultApplication: string, - fullFilePath: string -) { - // need to check if fullFilePath is file - const openWin = `"${overWriteDefaultApplication}" "${fullFilePath}"`; - childProcess.exec(openWin); -} - -function openMac(overWriteDefaultApplication: string, fullFilePath: string) { - const openFileOnMac = `open -a "${overWriteDefaultApplication}" "${fullFilePath}"`; - - // // need to check if fullFilePath is directory - childProcess.exec(openFileOnMac, { - cwd: `${overWriteDefaultApplication}` - }); -} - -export async function openPath(fullFilePath: string): Promise { - const overWriteDefaultApplication = (await appConfig.get( - DefaultImageApplicationSetting - )) as string; - // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor - return new Promise(async (resolve, reject) => { - // add extra test for photoshop - if ( - overWriteDefaultApplication - && OsBuildKey() === "mac" - && overWriteDefaultApplication.includes("Adobe Photoshop") - ) { - const shouldRunFirst = await ShouldRunFirst(); - if (!shouldRunFirst) { - // eslint-disable-next-line prefer-promise-reject-errors - reject( - "Photoshop is not running, please start photoshop first and try it again" - ); - return; - } - } - - if (overWriteDefaultApplication && OsBuildKey() === "mac") { - openMac(overWriteDefaultApplication, fullFilePath); - resolve(); - return; - } - - if (overWriteDefaultApplication && OsBuildKey() === "win") { - openWindows(overWriteDefaultApplication, fullFilePath); - resolve(); - return; - } +export async function OpenPath(fullFilePath: string): Promise { + return new Promise((resolve) => { logger.info("open default", fullFilePath); // eslint-disable-next-line @typescript-eslint/no-floating-promises shell.openPath(fullFilePath).then(() => { - resolve(); + resolve(true); }); }); } diff --git a/starskydesktop/src/app/ipc-bridge/ipc-bridge.spec.ts b/starskydesktop/src/app/ipc-bridge/ipc-bridge.spec.ts index 53d44f10ff..5e5af62e98 100644 --- a/starskydesktop/src/app/ipc-bridge/ipc-bridge.spec.ts +++ b/starskydesktop/src/app/ipc-bridge/ipc-bridge.spec.ts @@ -4,21 +4,15 @@ import { BrowserWindow, net } from "electron"; import * as appConfig from "electron-settings"; import { AppVersionIpcKey } from "../config/app-version-ipc-key.const"; -import { DefaultImageApplicationIpcKey } from "../config/default-image-application-settings-ipc-key.const"; import * as GetBaseUrlFromSettings from "../config/get-base-url-from-settings"; -import { - LocationIsRemoteIpcKey, - LocationUrlIpcKey, -} from "../config/location-ipc-keys.const"; +import { LocationIsRemoteIpcKey, LocationUrlIpcKey } from "../config/location-ipc-keys.const"; import { UpdatePolicyIpcKey } from "../config/update-policy-ipc-key.const"; -import * as fileSelectorWindow from "../file-selector-window/file-selector-window"; import * as SetupFileWatcher from "../file-watcher/setup-file-watcher"; import * as logger from "../logger/logger"; import * as createMainWindow from "../main-window/create-main-window"; import { mainWindows } from "../main-window/main-windows.const"; import { AppVersionCallback, - DefaultImageApplicationCallback, LocationIsRemoteCallback, LocationUrlCallback, UpdatePolicyCallback, @@ -88,9 +82,7 @@ describe("ipc bridge", () => { jest .spyOn(createMainWindow, "default") - .mockImplementationOnce(() => - Promise.resolve({ once: jest.fn() } as any) - ); + .mockImplementationOnce(() => Promise.resolve({ once: jest.fn() } as any)); jest .spyOn(SetupFileWatcher, "SetupFileWatcher") @@ -114,9 +106,7 @@ describe("ipc bridge", () => { jest .spyOn(createMainWindow, "default") - .mockImplementationOnce(() => - Promise.resolve({ once: jest.fn() } as any) - ); + .mockImplementationOnce(() => Promise.resolve({ once: jest.fn() } as any)); jest .spyOn(SetupFileWatcher, "SetupFileWatcher") @@ -180,15 +170,13 @@ describe("ipc bridge", () => { jest.spyOn(appConfig, "get").mockImplementationOnce(() => { return Promise.resolve(true); }); - jest - .spyOn(GetBaseUrlFromSettings, "GetBaseUrlFromSettings") - .mockImplementationOnce(() => { - return Promise.resolve({ - isLocal: true, - isValid: null, - location: "http://localhost:9609", - }); + jest.spyOn(GetBaseUrlFromSettings, "GetBaseUrlFromSettings").mockImplementationOnce(() => { + return Promise.resolve({ + isLocal: true, + isValid: null, + location: "http://localhost:9609", }); + }); await LocationUrlCallback(event, null); expect(event.reply).toHaveBeenCalled(); expect(event.reply).toHaveBeenCalledWith(LocationUrlIpcKey, { @@ -208,15 +196,13 @@ describe("ipc bridge", () => { jest.spyOn(appConfig, "get").mockImplementationOnce(() => { return Promise.resolve("__url_from_config__"); }); - jest - .spyOn(GetBaseUrlFromSettings, "GetBaseUrlFromSettings") - .mockImplementationOnce(() => { - return Promise.resolve({ - isLocal: false, - isValid: null, - location: "__url_from_config__", - }); + jest.spyOn(GetBaseUrlFromSettings, "GetBaseUrlFromSettings").mockImplementationOnce(() => { + return Promise.resolve({ + isLocal: false, + isValid: null, + location: "__url_from_config__", }); + }); await LocationUrlCallback(event, null); expect(event.reply).toHaveBeenCalled(); @@ -426,92 +412,4 @@ describe("ipc bridge", () => { expect(event.reply).toHaveBeenCalledWith(UpdatePolicyIpcKey, false); }); }); - - describe("DefaultImageApplicationCallback", () => { - it("getting with null input (DefaultImageApplicationCallback)", async () => { - const event = { reply: jest.fn() } as unknown as Electron.IpcMainEvent; - - jest.spyOn(appConfig, "get").mockReset(); - jest - .spyOn(appConfig, "get") - .mockImplementationOnce(() => Promise.resolve(null)); - await DefaultImageApplicationCallback(event, null); - expect(event.reply).toHaveBeenCalled(); - expect(event.reply).toHaveBeenCalledWith( - DefaultImageApplicationIpcKey, - null - ); - }); - - it("set reset of DefaultImageApplicationCallback", async () => { - const event = { reply: jest.fn() } as unknown as Electron.IpcMainEvent; - - jest.spyOn(appConfig, "get").mockImplementationOnce(() => { - return Promise.resolve(true); - }); - - jest.spyOn(appConfig, "set").mockImplementationOnce(() => { - return Promise.resolve(); - }); - - await DefaultImageApplicationCallback(event, { reset: true }); - - expect(event.reply).toHaveBeenCalled(); - expect(event.reply).toHaveBeenCalledWith( - DefaultImageApplicationIpcKey, - false - ); - }); - - it("should give successfull showOpenDialog", async () => { - const event = { reply: jest.fn() } as unknown as Electron.IpcMainEvent; - - jest.spyOn(appConfig, "get").mockImplementationOnce(() => { - return Promise.resolve(true); - }); - - jest - .spyOn(fileSelectorWindow, "fileSelectorWindow") - .mockImplementationOnce(() => - Promise.resolve(["result_from_fileSelectorWindow"]) - ); - - jest.spyOn(appConfig, "set").mockImplementationOnce(() => { - return Promise.resolve(); - }); - - await DefaultImageApplicationCallback(event, { showOpenDialog: true }); - - expect(event.reply).toHaveBeenCalled(); - expect(event.reply).toHaveBeenCalledWith( - DefaultImageApplicationIpcKey, - "result_from_fileSelectorWindow" - ); - }); - - it("should ignore failing showOpenDialog", async () => { - const event = { reply: jest.fn() } as unknown as Electron.IpcMainEvent; - - jest.spyOn(appConfig, "get").mockImplementationOnce(() => { - return Promise.resolve(true); - }); - - jest - .spyOn(fileSelectorWindow, "fileSelectorWindow") - // eslint-disable-next-line prefer-promise-reject-errors - .mockImplementationOnce(() => - Promise.reject(["result_from_fileSelectorWindow"]) - ); - - jest.spyOn(appConfig, "set").mockImplementationOnce(() => { - return Promise.resolve(); - }); - - await DefaultImageApplicationCallback(event, { - showOpenDialog: true, - }); - - expect(event.reply).toHaveBeenCalledTimes(0); - }); - }); }); diff --git a/starskydesktop/src/app/ipc-bridge/ipc-bridge.ts b/starskydesktop/src/app/ipc-bridge/ipc-bridge.ts index 762899e3f0..2036f53322 100644 --- a/starskydesktop/src/app/ipc-bridge/ipc-bridge.ts +++ b/starskydesktop/src/app/ipc-bridge/ipc-bridge.ts @@ -3,26 +3,17 @@ import { app, ipcMain } from "electron"; import * as appConfig from "electron-settings"; import { IlocationUrlSettings } from "../config/IlocationUrlSettings"; import { AppVersionIpcKey } from "../config/app-version-ipc-key.const"; -import DefaultImageApplicationSetting from "../config/default-image-application-settings"; -import { - DefaultImageApplicationIpcKey, - IDefaultImageApplicationProps -} from "../config/default-image-application-settings-ipc-key.const"; import { GetBaseUrlFromSettings } from "../config/get-base-url-from-settings"; -import { - LocationIsRemoteIpcKey, - LocationUrlIpcKey -} from "../config/location-ipc-keys.const"; +import { LocationIsRemoteIpcKey, LocationUrlIpcKey } from "../config/location-ipc-keys.const"; import { LocationIsRemoteSettingsKey, - LocationUrlSettingsKey + LocationUrlSettingsKey, } from "../config/location-settings.const"; import RememberUrl from "../config/remember-url-settings.const"; import { UpdatePolicyIpcKey } from "../config/update-policy-ipc-key.const"; import { UpdatePolicySettings } from "../config/update-policy-settings.const"; import UrlQuery from "../config/url-query"; import { ipRegex, urlRegex } from "../config/url-regex"; -import { fileSelectorWindow } from "../file-selector-window/file-selector-window"; import { SetupFileWatcher } from "../file-watcher/setup-file-watcher"; import logger from "../logger/logger"; import createMainWindow from "../main-window/create-main-window"; @@ -31,15 +22,10 @@ import { GetNetRequest } from "../net-request/get-net-request"; import { settingsWindows } from "../settings-window/settings-windows.const"; import { IsRemote } from "../warmup/is-remote"; -export async function UpdatePolicyCallback( - event: Electron.IpcMainEvent, - args: boolean, -) { +export async function UpdatePolicyCallback(event: Electron.IpcMainEvent, args: boolean) { if (args === null || args === undefined) { if (await appConfig.has(UpdatePolicySettings)) { - const updatePolicy = (await appConfig.get( - UpdatePolicySettings, - )) as boolean; + const updatePolicy = (await appConfig.get(UpdatePolicySettings)) as boolean; if (updatePolicy !== null && updatePolicy !== undefined) { event.reply(UpdatePolicyIpcKey, updatePolicy); @@ -73,10 +59,7 @@ async function closeAndCreateNewWindow() { }); } -export async function LocationIsRemoteCallback( - event: Electron.IpcMainEvent, - args: boolean, -) { +export async function LocationIsRemoteCallback(event: Electron.IpcMainEvent, args: boolean) { if (args !== undefined && args !== null) { await appConfig.set(LocationIsRemoteSettingsKey, args.toString()); // filewatcher need to be after update/set @@ -88,37 +71,26 @@ export async function LocationIsRemoteCallback( } export function AppVersionCallback(event: Electron.IpcMainEvent) { - const appVersion = app - .getVersion() - .match(/^[0-9]+\.[0-9]+/ig); + const appVersion = app.getVersion().match(/^[0-9]+\.[0-9]+/gi); event.reply(AppVersionIpcKey, appVersion); } -export async function LocationUrlCallback( - event: Electron.IpcMainEvent, - args: string, -) { +export async function LocationUrlCallback(event: Electron.IpcMainEvent, args: string) { // getting if (!args) { event.reply(LocationUrlIpcKey, await GetBaseUrlFromSettings()); return; } - if ( - args.match(urlRegex) - || args.match(ipRegex) - || args.startsWith("http://localhost:") - ) { + if (args.match(urlRegex) || args.match(ipRegex) || args.startsWith("http://localhost:")) { console.log("ipc-bridge start update"); // to avoid errors const locationUrl = args.replace(/\/$/, ""); try { - const response = await GetNetRequest( - locationUrl + new UrlQuery().HealthApi(), - ); + const response = await GetNetRequest(locationUrl + new UrlQuery().HealthApi()); const responseSettings = { location: locationUrl, isLocal: false, @@ -156,8 +128,13 @@ export async function LocationUrlCallback( } return; } - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.log(`ipc-bridge ${args.match(urlRegex)} ${args.match(ipRegex)} ${args.startsWith("http://localhost:")}`); + + console.log( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `ipc-bridge ${args.match(urlRegex)} ${args.match(ipRegex)} ${args.startsWith( + "http://localhost:" + )}` + ); event.reply(LocationUrlIpcKey, { isValid: false, @@ -166,43 +143,16 @@ export async function LocationUrlCallback( } as IlocationUrlSettings); } -export async function DefaultImageApplicationCallback( - event: Electron.IpcMainEvent, - args: IDefaultImageApplicationProps, -) { - if (!args) { - const currentSettings = await appConfig.get(DefaultImageApplicationSetting); - event.reply(DefaultImageApplicationIpcKey, currentSettings); - return; - } - if (args.reset) { - await appConfig.unset(DefaultImageApplicationSetting); - event.reply(DefaultImageApplicationIpcKey, false); - return; - } - - if (args.showOpenDialog) { - try { - const result = await fileSelectorWindow(); - await appConfig.set(DefaultImageApplicationSetting, result[0]); - event.reply(DefaultImageApplicationIpcKey, result[0]); - } catch (error) { // nothing here - } - } -} - function ipcBridge() { // When adding a new key also update preload-main.ts - ipcMain.on(LocationIsRemoteIpcKey, async (event, args : boolean) => LocationIsRemoteCallback(event, args)); + ipcMain.on(LocationIsRemoteIpcKey, async (event, args: boolean) => LocationIsRemoteCallback(event, args)); ipcMain.on(AppVersionIpcKey, (event) => AppVersionCallback(event)); ipcMain.on(LocationUrlIpcKey, async (event, args: string) => LocationUrlCallback(event, args)); ipcMain.on(UpdatePolicyIpcKey, async (event, args: boolean) => UpdatePolicyCallback(event, args)); - - ipcMain.on(DefaultImageApplicationIpcKey, async (event, args : IDefaultImageApplicationProps) => DefaultImageApplicationCallback(event, args)); } export default ipcBridge; diff --git a/starskydesktop/src/app/main-window/create-main-window.spec.ts b/starskydesktop/src/app/main-window/create-main-window.spec.ts index 391e52c62b..c3839952e9 100644 --- a/starskydesktop/src/app/main-window/create-main-window.spec.ts +++ b/starskydesktop/src/app/main-window/create-main-window.spec.ts @@ -3,7 +3,6 @@ import * as windowStateKeeper from "../window-state-keeper/window-state-keeper"; import createMainWindow from "./create-main-window"; import * as getNewFocusedWindow from "./get-new-focused-window"; -import * as onHeaderReceived from "./on-headers-received"; import * as saveRememberUrl from "./save-remember-url"; import * as spellCheck from "./spellcheck"; @@ -16,7 +15,20 @@ jest.mock("electron", () => { on: () => "en", }, // eslint-disable-next-line object-shorthand, func-names, @typescript-eslint/no-unused-vars - BrowserWindow: function (_x:object, _y: number, _w: number, _h: number, _s: boolean, _w2: object) { + BrowserWindow: function ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _x: object, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _y: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _w: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _h: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _s: boolean, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _w2: object + ) { return { id: 99, loadFile: jest.fn(), @@ -38,7 +50,6 @@ jest.mock("electron", () => { }; }, __esModule: true, - }; }); @@ -51,27 +62,21 @@ jest.mock("electron-settings", () => { describe("create main window", () => { it("create a new window (main)", async () => { - jest - .spyOn(windowStateKeeper, "windowStateKeeper") - .mockImplementationOnce(() => Promise.resolve({ - x: 0, - y: 0, - width: 1, - height: 1, - isMaximized: false, - track: jest.fn(), - })); + jest.spyOn(windowStateKeeper, "windowStateKeeper").mockImplementationOnce(() => Promise.resolve({ + x: 0, + y: 0, + width: 1, + height: 1, + isMaximized: false, + track: jest.fn(), + })); jest .spyOn(getNewFocusedWindow, "getNewFocusedWindow") .mockImplementationOnce(() => ({ x: 1, y: 1 })); - jest - .spyOn(onHeaderReceived, "onHeaderReceived") - .mockImplementationOnce(() => null); + jest.spyOn(spellCheck, "spellCheck").mockImplementationOnce(() => null); - jest - .spyOn(saveRememberUrl, "removeRememberUrl") - .mockImplementationOnce(() => null); + jest.spyOn(saveRememberUrl, "removeRememberUrl").mockImplementationOnce(() => null); jest .spyOn(saveRememberUrl, "saveRememberUrl") diff --git a/starskydesktop/src/app/main-window/create-main-window.ts b/starskydesktop/src/app/main-window/create-main-window.ts index fb25933ff0..b4037c5ff3 100644 --- a/starskydesktop/src/app/main-window/create-main-window.ts +++ b/starskydesktop/src/app/main-window/create-main-window.ts @@ -4,19 +4,15 @@ import { GetAppVersion } from "../config/get-app-version"; import { windowStateKeeper } from "../window-state-keeper/window-state-keeper"; import { getNewFocusedWindow } from "./get-new-focused-window"; import { mainWindows } from "./main-windows.const"; -import { onHeaderReceived } from "./on-headers-received"; import { removeRememberUrl, saveRememberUrl } from "./save-remember-url"; import { spellCheck } from "./spellcheck"; -async function createMainWindow( - openSpecificUrl: string, - offset = 0, -): Promise { +async function createMainWindow(openSpecificUrl: string, offset = 0): Promise { const mainWindowStateKeeper = await windowStateKeeper("main"); const { x, y } = getNewFocusedWindow( mainWindowStateKeeper.x - offset, - mainWindowStateKeeper.y - offset, + mainWindowStateKeeper.y - offset ); let newWindow = new BrowserWindow({ @@ -42,17 +38,13 @@ async function createMainWindow( mainWindowStateKeeper.track(newWindow); - const location = path.join( - __dirname, - "client/pages/redirect/reload-redirect.html", - ); + const location = path.join(__dirname, "client/pages/redirect/reload-redirect.html"); await newWindow.loadFile(location, { query: { "remember-url": openSpecificUrl }, }); spellCheck(newWindow); - onHeaderReceived(newWindow); newWindow.once("ready-to-show", () => { newWindow.show(); @@ -62,7 +54,7 @@ async function createMainWindow( console.log(url); return { - action: 'allow', + action: "allow", overrideBrowserWindowOptions: { webPreferences: { devTools: true, // allow diff --git a/starskydesktop/src/app/main-window/on-headers-received.ts b/starskydesktop/src/app/main-window/on-headers-received.ts deleted file mode 100644 index a24af7adcb..0000000000 --- a/starskydesktop/src/app/main-window/on-headers-received.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BrowserWindow } from "electron"; - -export function onHeaderReceived(newWindow: BrowserWindow) { - newWindow.webContents.session.webRequest.onHeadersReceived( - (res, callback) => { - // @TODO: re-enable - - // var currentSettings = appConfig.get("remote_settings_" + isPackaged()); - // var localhost = "http://localhost:9609 "; // with space on end - // ${appPort} - - // let whitelistDomain = localhost; - // if (currentSettings && currentSettings.location) { - // whitelistDomain = !currentSettings.location ? localhost : localhost + new URL(currentSettings.location).origin; - // } - - /// default-src 'none'; img-src 'self' https://a.tile.openstreetmap.org/ https://b.tile.openstreetmap.org/ https://c.tile.openstreetmap.org/; script-src 'self'; connect-src 'self' wss://starsky.server ;style-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; object-src 'none'; media-src 'self'; frame-src 'none'; manifest-src 'self'; block-all-mixed-content; - - // // When change also check if CSPMiddleware needs to be updated - // var csp = "default-src 'none'; img-src 'self' file://* https://www.openstreetmap.org https://tile.openstreetmap.org https://*.tile.openstreetmap.org " - // + whitelistDomain + "; " + "style-src file://* unsafe-inline https://www.openstreetmap.org " + whitelistDomain - // + "; script-src 'self' https://js.monitor.azure.com/scripts/b/ai.2.min.js file://* https://az416426.vo.msecnd.net; " + - // "connect-src 'self' https://dc.services.visualstudio.com/v2/track https://*.in.applicationinsights.azure.com//v2/track " + whitelistDomain + "; " + - // "font-src file://* " + whitelistDomain + "; media-src " + whitelistDomain + ";"; - - // if (!res.url.startsWith('devtools://') && !res.url.startsWith('http://localhost:3000/') ) { - // res.responseHeaders["Content-Security-Policy"] = csp; - // } - - callback({ cancel: false, responseHeaders: res.responseHeaders }); - } - ); -} diff --git a/starskydesktop/src/app/menu/app-menu.ts b/starskydesktop/src/app/menu/app-menu.ts index 0b20d221a7..18becb8d14 100644 --- a/starskydesktop/src/app/menu/app-menu.ts +++ b/starskydesktop/src/app/menu/app-menu.ts @@ -6,6 +6,27 @@ import { EditFile } from "../edit-file/edit-file"; import { IsDutch } from "../i18n/i18n"; import createMainWindow from "../main-window/create-main-window"; import { createSettingsWindow } from "../settings-window/create-settings-window"; +import { IsRemote } from "../warmup/is-remote"; + +function sendKeybinding(win: BrowserWindow, keyCode: string, cmdOrCtrl: boolean, shift: boolean) { + const modifiers = []; + + if (cmdOrCtrl) { + const isMac = process.platform === "darwin"; + if (isMac) { + modifiers.push("meta"); + } else { + modifiers.push("ctrl"); + } + } + if (shift) { + modifiers.push("shift"); + } + + win.webContents.sendInputEvent({ type: "keyDown", modifiers, keyCode }); + win.webContents.sendInputEvent({ type: "char", modifiers, keyCode }); + win.webContents.sendInputEvent({ type: "keyUp", modifiers, keyCode }); +} function AppMenu() { const isMac = process.platform === "darwin"; @@ -60,7 +81,14 @@ function AppMenu() { click: () => { const focusWindow = BrowserWindow.getFocusedWindow(); // eslint-disable-next-line @typescript-eslint/no-floating-promises - if (focusWindow) EditFile(focusWindow); + IsRemote().then(async (isRemote) => { + if (!isRemote) { + sendKeybinding(focusWindow, "e", true, false); + return; + } + + if (focusWindow) await EditFile(focusWindow).catch(() => {}); + }); }, accelerator: "CmdOrCtrl+E", }, @@ -116,13 +144,21 @@ function AppMenu() { label: IsDutch() ? "Instellingen" : "Settings", submenu: [ { - label: IsDutch() ? "Instellingen" : "Settings", + label: IsDutch() ? "Verbindings instellingen" : "Connection Settings", click: () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises createSettingsWindow(); }, accelerator: "CmdOrCtrl+,", }, + { + label: IsDutch() ? "App instellingen" : "App Settings", + click: () => { + const focusWindow = BrowserWindow.getFocusedWindow(); + sendKeybinding(focusWindow, "k", true, true); + }, + accelerator: "CmdOrCtrl+shift+k", + }, ], }, { @@ -151,9 +187,7 @@ function AppMenu() { label: "Open in browser", // eslint-disable-next-line @typescript-eslint/no-misused-promises click: async () => { - await shell.openExternal( - BrowserWindow.getFocusedWindow().webContents.getURL() - ); + await shell.openExternal(BrowserWindow.getFocusedWindow().webContents.getURL()); }, }, ], @@ -189,12 +223,7 @@ function AppMenu() { }, { role: "zoom" }, ...(isMac - ? [ - { type: "separator" }, - { role: "front" }, - { type: "separator" }, - { role: "window" }, - ] + ? [{ type: "separator" }, { role: "front" }, { type: "separator" }, { role: "window" }] : [{ role: "close" }]), ], }, @@ -205,7 +234,7 @@ function AppMenu() { label: "Documentation website", // eslint-disable-next-line @typescript-eslint/no-misused-promises click: async () => { - await shell.openExternal("https://docs.qdraw.nl/download"); + await shell.openExternal("https://docs.qdraw.nl/docs/getting-started/first-steps"); }, }, { @@ -213,9 +242,7 @@ function AppMenu() { // Referenced from HealthCheckForUpdates // eslint-disable-next-line @typescript-eslint/no-misused-promises click: async () => { - await shell.openExternal( - "https://github.com/qdraw/starsky/releases/latest" - ); + await shell.openExternal("https://github.com/qdraw/starsky/releases/latest"); }, }, ], diff --git a/starskydesktop/src/client/pages/settings/settings.html b/starskydesktop/src/client/pages/settings/settings.html index 410dd3ab9d..413f50222c 100644 --- a/starskydesktop/src/client/pages/settings/settings.html +++ b/starskydesktop/src/client/pages/settings/settings.html @@ -46,18 +46,6 @@ -
Select default application
-
-   - -

 

-
- Loading... -
-
Check for updates