Repository: JanDeDobbeleer/oh-my-posh
Pull request #4379 (2023-10-25 22:00:45)

Update macos.mdx
fix(docs): fix missing link and misspelled word

### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [ ] Tests for the changes have been added (for bug fixes / features).
- [ ] Docs have been added/updated (for bug fixes / features).

### Description

<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at d3055fd</samp>

Fix typo and link in macOS installation docs. The pull request corrects a misspelled word and updates a link to the iTerm2 website in the `macos.mdx` file.

### How

<!--
copilot:walkthrough
-->
### <samp>🤖 Generated by Copilot at d3055fd</samp>

* Fix a grammatical error in the sentence "Oh my posh support iTerm2" by changing "support" to "supports" ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4379/files?diff=unified&w=0#diff-5318aa49951fe4475354c530eaa6c8f4c792a73cf7e36fd3d42e96529f1ea63dL14-R14))
* Add a missing link reference for [iterm2] at the bottom of the file, pointing to the official website of iTerm2 ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4379/files?diff=unified&w=0#diff-5318aa49951fe4475354c530eaa6c8f4c792a73cf7e36fd3d42e96529f1ea63dL28-R29))

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 00063d39..049450e6 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1964,7 +1964,9 @@
       "avatar_url": "https://avatars.githubusercontent.com/u/1389894?v=4",
       "profile": "http://creativewebspecialist.co.uk",
       "contributions": [
-        "code"
+        "code",
+        "design",
+        "doc"
       ]
     },
     {
@@ -2002,6 +2004,17 @@
       "contributions": [
         "code"
       ]
+    },
+    {
+      "login": "mateusz-bajorek",
+      "name": "Mateusz Bajorek",
+      "avatar_url": "https://avatars.githubusercontent.com/u/11185738?v=4",
+      "profile": "https://github.com/mateusz-bajorek",
+      "contributions": [
+        "code",
+        "design",
+        "doc"
+      ]
     }
   ],
   "contributorsPerLine": 7,
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 80fafc1b..a2107745 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -232,6 +232,8 @@ const (
 	TIME SegmentType = "time"
 	// UI5 Tooling segment
 	UI5TOOLING SegmentType = "ui5tooling"
+	// UMBRACO writes the Umbraco version if Umbraco is present
+	UMBRACO SegmentType = "umbraco"
 	// UNITY writes which Unity version is currently active
 	UNITY SegmentType = "unity"
 	// UPGRADE lets you know if you can upgrade Oh My Posh
@@ -323,6 +325,7 @@ var Segments = map[SegmentType]func() SegmentWriter{
 	TEXT:          func() SegmentWriter { return &segments.Text{} },
 	TIME:          func() SegmentWriter { return &segments.Time{} },
 	UI5TOOLING:    func() SegmentWriter { return &segments.UI5Tooling{} },
+	UMBRACO:       func() SegmentWriter { return &segments.Umbraco{} },
 	UNITY:         func() SegmentWriter { return &segments.Unity{} },
 	UPGRADE:       func() SegmentWriter { return &segments.Upgrade{} },
 	VALA:          func() SegmentWriter { return &segments.Vala{} },
diff --git a/src/segments/node.go b/src/segments/node.go
index 3910caf8..79fad8f1 100644
--- a/src/segments/node.go
+++ b/src/segments/node.go
@@ -55,7 +55,7 @@ func (n *Node) loadContext() {
 		return
 	}
 	if n.language.env.HasFiles("yarn.lock") {
-		n.PackageManagerIcon = n.language.props.GetString(YarnIcon, "\uF61A")
+		n.PackageManagerIcon = n.language.props.GetString(YarnIcon, "\uF011B")
 		return
 	}
 	if n.language.env.HasFiles("package-lock.json") || n.language.env.HasFiles("package.json") {
diff --git a/src/segments/umbraco.go b/src/segments/umbraco.go
new file mode 100644
index 00000000..5c3209fd
--- /dev/null
+++ b/src/segments/umbraco.go
@@ -0,0 +1,159 @@
+package segments
+
+import (
+	"encoding/xml"
+	"path/filepath"
+	"strings"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+	"github.com/jandedobbeleer/oh-my-posh/src/properties"
+)
+
+type Umbraco struct {
+	props properties.Properties
+	env   platform.Environment
+
+	Modern  bool
+	Version string
+}
+
+type CSProj struct {
+	PackageReferences []struct {
+		Name    string `xml:"include,attr"`
+		Version string `xml:"version,attr"`
+	} `xml:"ItemGroup>PackageReference"`
+}
+
+type WebConfig struct {
+	AppSettings []struct {
+		Key   string `xml:"key,attr"`
+		Value string `xml:"value,attr"`
+	} `xml:"appSettings>add"`
+}
+
+func (u *Umbraco) Enabled() bool {
+	var location string
+
+	// Check if we have a folder called Umbraco or umbraco in the current directory or a parent directory
+	folders := []string{"umbraco", "Umbraco"}
+	for _, folder := range folders {
+		if file, err := u.env.HasParentFilePath(folder); err == nil {
+			location = file.ParentFolder
+			break
+		}
+	}
+
+	if len(location) == 0 {
+		u.env.Debug("No umbraco folder found in parent directories")
+		return false
+	}
+
+	files := u.env.LsDir(location)
+
+	// Loop over files where we found the Umbraco folder
+	// To see if we can find a web.config or *.csproj file
+	// If we do then we can scan the file to see if Umbraco has been installed
+	for _, file := range files {
+		if file.IsDir() {
+			continue
+		}
+
+		if strings.EqualFold(file.Name(), "web.config") {
+			return u.TryFindLegacyUmbraco(filepath.Join(location, file.Name()))
+		}
+
+		if strings.EqualFold(filepath.Ext(file.Name()), ".csproj") {
+			return u.TryFindModernUmbraco(filepath.Join(location, file.Name()))
+		}
+	}
+
+	return false
+}
+
+func (u *Umbraco) Template() string {
+	return "{{.Version}} "
+}
+
+func (u *Umbraco) Init(props properties.Properties, env platform.Environment) {
+	u.props = props
+	u.env = env
+}
+
+func (u *Umbraco) TryFindModernUmbraco(configPath string) bool {
+	// Check the passed in filepath is not empty
+	if len(configPath) == 0 {
+		u.env.Debug("UMBRACO: No .CSProj file path passed in")
+		return false
+	}
+
+	// Read the file contents of the csproj file
+	contents := u.env.FileContent(configPath)
+
+	// As XML unmarshal does not support case insenstivity attributes
+	// this is just a simple string replace to lowercase the attribute
+	contents = strings.ReplaceAll(contents, "Include=", "include=")
+	contents = strings.ReplaceAll(contents, "Version=", "version=")
+
+	// XML Unmarshal - map the contents of the file to the CSProj struct
+	csProjPackages := CSProj{}
+	err := xml.Unmarshal([]byte(contents), &csProjPackages)
+
+	if err != nil {
+		u.env.Debug("UMBRACO: Error while trying to parse XML of .csproj file")
+		u.env.Debug(err.Error())
+	}
+
+	// Loop over all the package references
+	for _, packageReference := range csProjPackages.PackageReferences {
+		if strings.EqualFold(packageReference.Name, "umbraco.cms") {
+			u.Modern = true
+			u.Version = packageReference.Version
+
+			return true
+		}
+	}
+
+	return false
+}
+
+func (u *Umbraco) TryFindLegacyUmbraco(configPath string) bool {
+	// Check the passed in filepath is not empty
+	if len(configPath) == 0 {
+		u.env.Debug("UMBRACO: No web.config file path passed in")
+		return false
+	}
+
+	// Read the file contents of the web.config
+	contents := u.env.FileContent(configPath)
+
+	// As XML unmarshal does not support case insenstivity attributes
+	// this is just a simple string replace to lowercase the attribute
+	contents = strings.ReplaceAll(contents, "Key=", "key=")
+	contents = strings.ReplaceAll(contents, "Value=", "value=")
+
+	// XML Unmarshal - web.config all AppSettings keys
+	webConfigAppSettings := WebConfig{}
+	err := xml.Unmarshal([]byte(contents), &webConfigAppSettings)
+
+	if err != nil {
+		u.env.Debug("UMBRACO: Error while trying to parse XML of web.config file")
+		u.env.Debug(err.Error())
+	}
+
+	// Loop over all the package references
+	for _, appSetting := range webConfigAppSettings.AppSettings {
+		if strings.EqualFold(appSetting.Key, "umbraco.core.configurationstatus") {
+			u.Modern = false
+
+			if len(appSetting.Value) == 0 {
+				u.Version = "Unknown"
+			} else {
+				u.Version = appSetting.Value
+			}
+
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/themes/schema.json b/themes/schema.json
index 02a08af1..53979b8b 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -310,6 +310,7 @@
             "text",
             "terraform",
             "ui5tooling",
+            "umbraco",
             "unity",
             "upgrade",
             "wakatime",
@@ -1499,7 +1500,7 @@
                     "type": "string",
                     "title": "Yarn Icon",
                     "description": "Icon/text to use for Yarn",
-                    "default": " \uF61A"
+                    "default": " \uF011B"
                   },
                   "npm_icon": {
                     "type": "string",
@@ -3284,6 +3285,29 @@
               }
             }
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "umbraco" }
+            }
+          },
+          "then": {
+            "title": "Display something umbraco",
+            "description": "https://ohmyposh.dev/docs/segments/umbraco",
+            "properties": {
+              "properties": {
+                "properties": {
+                  "newprop": {
+                    "type": "string",
+                    "title": "New Property",
+                    "description": "the default text to display",
+                    "default": "Hello"
+                  }
+                }
+              }
+            }
+          }
         }
       ]
     }
diff --git a/website/docs/contributors.md b/website/docs/contributors.md
index 51920696..7e446d4b 100644
--- a/website/docs/contributors.md
+++ b/website/docs/contributors.md
@@ -283,11 +283,12 @@ Thanks goes to these wonderful people ([emoji key][acek]):
     </tr>
     <tr>
       <td align="center" valign="top" width="14.28%"><a href="https://github.com/deepak-dev-96"><img src="https://avatars.githubusercontent.com/u/134447761?v=4?s=100" width="100px;" alt="Deepak Dev"/><br /><sub><b>Deepak Dev</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=deepak-dev-96" title="Documentation">📖</a></td>
-      <td align="center" valign="top" width="14.28%"><a href="http://creativewebspecialist.co.uk"><img src="https://avatars.githubusercontent.com/u/1389894?v=4?s=100" width="100px;" alt="Warren Buckley"/><br /><sub><b>Warren Buckley</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=warrenbuckley" title="Code">💻</a></td>
+      <td align="center" valign="top" width="14.28%"><a href="http://creativewebspecialist.co.uk"><img src="https://avatars.githubusercontent.com/u/1389894?v=4?s=100" width="100px;" alt="Warren Buckley"/><br /><sub><b>Warren Buckley</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=warrenbuckley" title="Code">💻</a> <a href="#design-warrenbuckley" title="Design">🎨</a> <a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=warrenbuckley" title="Documentation">📖</a></td>
       <td align="center" valign="top" width="14.28%"><a href="https://github.com/LunarMarathon"><img src="https://avatars.githubusercontent.com/u/113847439?v=4?s=100" width="100px;" alt="LunarMarathon"/><br /><sub><b>LunarMarathon</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=LunarMarathon" title="Documentation">📖</a></td>
       <td align="center" valign="top" width="14.28%"><a href="https://ginglis.me"><img src="https://avatars.githubusercontent.com/u/43075615?v=4?s=100" width="100px;" alt="Gavin Inglis"/><br /><sub><b>Gavin Inglis</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=ginglis13" title="Code">💻</a></td>
       <td align="center" valign="top" width="14.28%"><a href="http://jaliyaudagedara.blogspot.com"><img src="https://avatars.githubusercontent.com/u/5653381?v=4?s=100" width="100px;" alt="Jaliya Udagedara"/><br /><sub><b>Jaliya Udagedara</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=jaliyaudagedara" title="Documentation">📖</a></td>
       <td align="center" valign="top" width="14.28%"><a href="https://github.com/BPplays"><img src="https://avatars.githubusercontent.com/u/58504799?v=4?s=100" width="100px;" alt="BPplays"/><br /><sub><b>BPplays</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=BPplays" title="Code">💻</a></td>
+      <td align="center" valign="top" width="14.28%"><a href="https://github.com/mateusz-bajorek"><img src="https://avatars.githubusercontent.com/u/11185738?v=4?s=100" width="100px;" alt="Mateusz Bajorek"/><br /><sub><b>Mateusz Bajorek</b></sub></a><br /><a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=mateusz-bajorek" title="Code">💻</a> <a href="#design-mateusz-bajorek" title="Design">🎨</a> <a href="https://github.com/JanDeDobbeleer/oh-my-posh/commits?author=mateusz-bajorek" title="Documentation">📖</a></td>
     </tr>
   </tbody>
 </table>
diff --git a/website/docs/installation/macos.mdx b/website/docs/installation/macos.mdx
index 066a5596..7e522f2c 100644
--- a/website/docs/installation/macos.mdx
+++ b/website/docs/installation/macos.mdx
@@ -11,7 +11,7 @@ import Next from "./next.mdx";
 
 ## Set up your terminal
 
-As the standard terminal only support [256 colors][256-colors], we advise using
+As the standard terminal only supports [256 colors][256-colors], we advise using
 [iTerm2][iterm2] or any other modern day macOS terminal that supports ANSI characters.
 
 :::info
@@ -25,4 +25,5 @@ To display all icons, we recommend the use of a [Nerd Font][fonts].
 <Next />
 
 [256-colors]: /docs/configuration/colors#standard-colors
-[fonts]: /docs/installation/fonts
\ No newline at end of file
+[iterm2]: https://iterm2.com
+[fonts]: /docs/installation/fonts
diff --git a/website/docs/segments/node.mdx b/website/docs/segments/node.mdx
index e3b77423..3e2b1084 100644
--- a/website/docs/segments/node.mdx
+++ b/website/docs/segments/node.mdx
@@ -33,7 +33,7 @@ import Config from "@site/src/components/Config.js";
 | `display_mode`          | `string`  | <ul><li>`always`: The segment is always displayed</li><li>`files`: The segment is only displayed when one of the following files is present (**default**):<ul><li>`*.js`</li><li>`*.ts`</li><li>`package.json`</li><li>`.nvmrc`</li><li>`pnpm-workspace.yaml`</li><li>`.pnpmfile.cjs`</li><li>`.npmrc`</li><li>`.vue`</li></ul></li></ul> |
 | `version_url_template`  | `string`  | a go [text/template][go-text-template] [template][templates] that creates the URL of the version info / release notes                                                                                                                                                                                                                     |
 | `fetch_package_manager` | `boolean` | define if the current project uses Yarn or NPM - defaults to `false`                                                                                                                                                                                                                                                                      |
-| `yarn_icon`             | `string`  | the icon/text to display when using Yarn - defaults to ` \uF61A`                                                                                                                                                                                                                                                                          |
+| `yarn_icon`             | `string`  | the icon/text to display when using Yarn - defaults to ` \uF011B`                                                                                                                                                                                                                                                                          |
 | `npm_icon`              | `string`  | the icon/text to display when using NPM - defaults to ` \uE71E`                                                                                                                                                                                                                                                                           |
 
 ## Template ([info][templates])
diff --git a/website/docs/segments/umbraco.mdx b/website/docs/segments/umbraco.mdx
new file mode 100644
index 00000000..cc403fe0
--- /dev/null
+++ b/website/docs/segments/umbraco.mdx
@@ -0,0 +1,56 @@
+---
+id: umbraco
+title: Umbraco
+sidebar_label: Umbraco
+---
+
+## What
+
+Display current Umbraco Version if found inside the current working directory.
+The segment will only show based on the following logic
+
+* The current folder contains the folder named umbraco
+* Modern Umbraco (.NET Core)
+    * Check to see if current folder contains one or more .csproj files
+    * Open .csproj XML files and check to see if Umbraco is installed as a PackageReference
+    * Read the installed version
+* Legacy Umbraco (.NET Framework)
+    * Check to see if the current folder contains a web.config
+    * Open the XML and look for AppSettings keys
+    * If umbraco is installed it has a setting called umbraco.core.configurationstatus
+    * Read the value inside this AppSetting to get its version
+
+## Sample Configuration
+
+import Config from '@site/src/components/Config.js';
+
+<Config data={{
+  "type": "umbraco",
+  "background": "#ffffff",
+  "foreground": "#d886f1",
+  "style": "diamond",
+  "leading_diamond": "\ue0b6",
+  "trailing_diamond": "\ue0b4",
+  "template": "\udb81\udd49 {{ .Version }}",
+  "background_templates": [
+    "{{ if (.Modern) }}#3544B1{{ end }}",
+    "{{ if not (.Modern) }}#F79C37{{ end }}"
+  ]
+}}/>
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ .Version }}
+```
+
+:::
+
+## Properties
+
+| Name           | Type      | Description                                                                                                         |
+| -------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
+| `.Modern`      | `boolean` | a boolean to detemine if this is modern Umbraco V9+ using modern .NET or if its legacy Umbraco using .NET Framework |
+| `.Version`     | `string`  | the version of umbraco found                                                                                       |
diff --git a/website/sidebars.js b/website/sidebars.js
index 22bfabad..dfeba6bb 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -120,6 +120,7 @@ module.exports = {
         "segments/text",
         "segments/time",
         "segments/ui5tooling",
+        "segments/umbraco",
         "segments/unity",
         "segments/upgrade",
         "segments/vala",


Test patch:
diff --git a/src/segments/umbraco_test.go b/src/segments/umbraco_test.go
new file mode 100644
index 00000000..c26deead
--- /dev/null
+++ b/src/segments/umbraco_test.go
@@ -0,0 +1,128 @@
+package segments
+
+import (
+	"errors"
+	"io/fs"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/mock"
+	"github.com/jandedobbeleer/oh-my-posh/src/platform"
+
+	"github.com/stretchr/testify/assert"
+	mock2 "github.com/stretchr/testify/mock"
+)
+
+func TestUmbracoSegment(t *testing.T) {
+	cases := []struct {
+		Case             string
+		ExpectedEnabled  bool
+		ExpectedString   string
+		Template         string
+		HasUmbracoFolder bool
+		HasCsproj        bool
+		HasWebConfig     bool
+	}{
+		{
+			Case:             "No Umbraco folder found",
+			HasUmbracoFolder: false,
+			ExpectedEnabled:  false, // Segment should not be enabled
+		},
+		{
+			Case:             "Umbraco Folder but NO web.config or .csproj",
+			HasUmbracoFolder: true,
+			HasCsproj:        false,
+			HasWebConfig:     false,
+			ExpectedEnabled:  false, // Segment should not be enabled
+		},
+		{
+			Case:             "Umbraco Folder and web.config but NO .csproj",
+			HasUmbracoFolder: true,
+			HasCsproj:        false,
+			HasWebConfig:     true,
+			ExpectedEnabled:  true, // Segment should be enabled and visible
+			Template:         "{{ .Version }}",
+			ExpectedString:   "8.18.9",
+		},
+		{
+			Case:             "Umbraco Folder and .csproj but NO web.config",
+			HasUmbracoFolder: true,
+			HasCsproj:        true,
+			HasWebConfig:     false,
+			ExpectedEnabled:  true, // Segment should be enabled and visible
+			Template:         "{{ .Version }}",
+			ExpectedString:   "12.1.2",
+		},
+		{
+			Case:             "Umbraco Folder and .csproj with custom template",
+			HasUmbracoFolder: true,
+			HasCsproj:        true,
+			ExpectedEnabled:  true,
+			Template:         "Version:{{ .Version }} ModernUmbraco:{{ .Modern }}",
+			ExpectedString:   "Version:12.1.2 ModernUmbraco:true",
+		},
+	}
+
+	for _, tc := range cases {
+		// Prepare/arrange the test
+		env := new(mock.MockedEnvironment)
+		var sampleCSProj, sampleWebConfig string
+
+		if tc.HasCsproj {
+			content, _ := os.ReadFile("../test/umbraco/MyProject.csproj")
+			sampleCSProj = string(content)
+		}
+		if tc.HasWebConfig {
+			content, _ := os.ReadFile("../test/umbraco/web.config")
+			sampleWebConfig = string(content)
+		}
+
+		const umbracoProjectDirectory = "/workspace/MyProject"
+		env.On("Pwd").Return(umbracoProjectDirectory)
+		env.On("FileContent", filepath.Join(umbracoProjectDirectory, "MyProject.csproj")).Return(sampleCSProj)
+		env.On("FileContent", filepath.Join(umbracoProjectDirectory, "web.config")).Return(sampleWebConfig)
+		env.On("Debug", mock2.Anything)
+
+		if tc.HasUmbracoFolder {
+			fileInfo := &platform.FileInfo{
+				Path:         "/workspace/MyProject/Umbraco",
+				ParentFolder: "/workspace/MyProject",
+				IsDir:        true,
+			}
+
+			env.On("HasParentFilePath", "umbraco").Return(fileInfo, nil)
+		} else {
+			env.On("HasParentFilePath", "Umbraco").Return(&platform.FileInfo{}, errors.New("no such file or directory"))
+			env.On("HasParentFilePath", "umbraco").Return(&platform.FileInfo{}, errors.New("no such file or directory"))
+		}
+
+		dirEntries := []fs.DirEntry{}
+		if tc.HasCsproj {
+			dirEntries = append(dirEntries, &MockDirEntry{
+				name:  "MyProject.csproj",
+				isDir: false,
+			})
+		}
+
+		if tc.HasWebConfig {
+			dirEntries = append(dirEntries, &MockDirEntry{
+				name:  "web.config",
+				isDir: false,
+			})
+		}
+
+		env.On("LsDir", umbracoProjectDirectory).Return(dirEntries)
+
+		// Setup the Umbraco segment with the mocked environment & properties
+		umb := &Umbraco{
+			env: env,
+		}
+
+		// Assert the test results
+		// Check if the segment should be enabled and
+		// the rendered string matches what we expect when specifying a template for the segment
+		assert.Equal(t, tc.ExpectedEnabled, umb.Enabled(), tc.Case)
+		assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, umb), tc.Case)
+	}
+}
diff --git a/src/test/umbraco/MyProject.csproj b/src/test/umbraco/MyProject.csproj
new file mode 100644
index 00000000..9f990a6e
--- /dev/null
+++ b/src/test/umbraco/MyProject.csproj
@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="uMarketingSuite" Version="1.23.0" />
+        <PackageReference Include="Umbraco.TheStarterKit" Version="11.0.0" />
+        <PackageReference Include="Umbraco.Cms" Version="12.1.2" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <!-- Opt-in to app-local ICU to ensure consistent globalization APIs across different platforms -->
+        <PackageReference Include="Microsoft.ICU.ICU4C.Runtime" Version="68.2.0.9"/>
+        <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="68.2.0.9" Condition="$(RuntimeIdentifier.StartsWith('linux')) or $(RuntimeIdentifier.StartsWith('win')) or ('$(RuntimeIdentifier)' == '' and !$([MSBuild]::IsOSPlatform('osx')))"/>
+    </ItemGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\uMarketingSuite.StarterKit\uMarketingSuite.StarterKit.csproj" />
+    </ItemGroup>
+
+    <PropertyGroup>
+        <!-- Razor files are needed for the backoffice to work correctly -->
+        <CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
+    </PropertyGroup>
+
+    <PropertyGroup>
+        <!-- Remove RazorCompileOnBuild and RazorCompileOnPublish when not using ModelsMode InMemoryAuto -->
+        <RazorCompileOnBuild>false</RazorCompileOnBuild>
+        <RazorCompileOnPublish>false</RazorCompileOnPublish>
+    </PropertyGroup>
+
+</Project>
diff --git a/src/test/umbraco/web.config b/src/test/umbraco/web.config
new file mode 100644
index 00000000..7cb15417
--- /dev/null
+++ b/src/test/umbraco/web.config
@@ -0,0 +1,325 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration>
+    <!--
+        Define the Web.config template, which is used when creating the initial Web.config,
+        and then transforms from web.Template.[Debug|Release].config are applied.
+        Documentation for Web.config at: https://our.umbraco.com/documentation/Reference/Config/webconfig/
+    -->
+
+    <configSections>
+        <section name="clientDependency" type="ClientDependency.Core.Config.ClientDependencySection, ClientDependency.Core" requirePermission="false"/>
+
+        <sectionGroup name="umbracoConfiguration">
+            <section name="settings" type="Umbraco.Core.Configuration.UmbracoSettings.UmbracoSettingsSection, Umbraco.Core" requirePermission="false"/>
+            <section name="HealthChecks" type="Umbraco.Core.Configuration.HealthChecks.HealthChecksSection, Umbraco.Core" requirePermission="false"/>
+        </sectionGroup>
+
+        <sectionGroup name="imageProcessor">
+            <section name="security" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageSecuritySection, ImageProcessor.Web"/>
+            <section name="processing" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageProcessingSection, ImageProcessor.Web"/>
+            <section name="caching" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web"/>
+        </sectionGroup>
+    </configSections>
+
+    <umbracoConfiguration>
+        <settings configSource="config\umbracoSettings.config"/>
+        <HealthChecks configSource="config\HealthChecks.config"/>
+    </umbracoConfiguration>
+
+    <clientDependency configSource="config\ClientDependency.config"/>
+
+    <appSettings>
+        <add key="Umbraco.Core.ConfigurationStatus" value="8.18.9"/>
+        <add key="Umbraco.Core.ReservedUrls" value=""/>
+        <add key="Umbraco.Core.ReservedPaths" value=""/>
+        <add key="Umbraco.Core.Path" value="~/umbraco"/>
+        <add key="Umbraco.Core.HideTopLevelNodeFromPath" value="true"/>
+        <add key="Umbraco.Core.TimeOutInMinutes" value="20"/>
+        <add key="Umbraco.Core.DefaultUILanguage" value="en-US"/>
+        <add key="Umbraco.Core.UseHttps" value="false"/>
+        <add key="Umbraco.Core.AllowContentDashboardAccessToAllUsers" value="true"/>
+        <add key="Umbraco.Core.ContentDashboardUrl-Allowlist" value=""/>
+        <add key="Umbraco.Core.HelpPage-Allowlist" value=""/>
+
+        <add key="ValidationSettings:UnobtrusiveValidationMode" value="None"/>
+        <add key="webpages:Enabled" value="false"/>
+        <add key="enableSimpleMembership" value="false"/>
+        <add key="autoFormsAuthentication" value="false"/>
+        <add key="dataAnnotations:dataTypeAttribute:disableRegEx" value="false"/>
+
+        <add key="owin:appStartup" value="UmbracoDefaultOwinStartup"/>
+
+        <add key="Umbraco.ModelsBuilder.Enable" value="true"/>
+        <add key="Umbraco.ModelsBuilder.ModelsMode" value="PureLive"/>
+        <add key="Umbraco.Web.PublishedCache.NuCache.Serializer" value="MsgPack"/>
+        <add key="Umbraco.Web.SanitizeTinyMce" value="true"/>
+    </appSettings>
+
+    <!--
+        Important: if you're upgrading Umbraco, do not clear the connectionString/providerName during your Web.config merge.
+    -->
+    <connectionStrings>
+        <remove name="umbracoDbDSN"/>
+        <add name="umbracoDbDSN" connectionString="" providerName=""/>
+    </connectionStrings>
+
+    <system.data>
+        <DbProviderFactories>
+            <remove invariant="System.Data.SqlServerCe.4.0"/>
+            <add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0"
+                description=".NET Framework Data Provider for Microsoft SQL Server Compact"
+                type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe"/>
+        </DbProviderFactories>
+    </system.data>
+
+    <system.net>
+        <mailSettings>
+            <!--
+                If you need Umbraco to send out system mails (like reset password and invite user),
+                you must configure your SMTP settings here - for example:
+            -->
+            <!--
+            <smtp from="noreply@example.com" deliveryMethod="Network">
+                <network host="localhost" port="25" enableSsl="false" userName="" password="" />
+            </smtp>
+            -->
+        </mailSettings>
+    </system.net>
+
+    <system.web>
+        <customErrors mode="RemoteOnly"/>
+
+        <trace enabled="false" requestLimit="10" pageOutput="false" traceMode="SortByTime" localOnly="true"/>
+
+        <httpRuntime requestValidationMode="2.0" enableVersionHeader="false" targetFramework="4.7.2" maxRequestLength="51200" fcnMode="Single"/>
+
+        <httpModules>
+            <add name="ScriptModule"
+                type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
+            <add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web"/>
+            <add name="ClientDependencyModule" type="ClientDependency.Core.Module.ClientDependencyModule, ClientDependency.Core"/>
+            <add name="ImageProcessorModule" type="ImageProcessor.Web.HttpModules.ImageProcessingModule, ImageProcessor.Web"/>
+        </httpModules>
+
+        <httpHandlers>
+            <remove verb="*" path="*.asmx"/>
+            <add verb="*" path="*.asmx"
+                type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
+                validate="false"/>
+            <add verb="*" path="*_AppService.axd"
+                type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
+                validate="false"/>
+            <add verb="GET,HEAD" path="ScriptResource.axd"
+                type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
+                validate="false"/>
+            <add verb="*" path="DependencyHandler.axd" type="ClientDependency.Core.CompositeFiles.CompositeDependencyHandler, ClientDependency.Core "/>
+        </httpHandlers>
+
+        <compilation defaultLanguage="c#" debug="false" batch="true" targetFramework="4.7.2" numRecompilesBeforeAppRestart="50"/>
+
+        <authentication mode="Forms">
+            <forms name="yourAuthCookie" loginUrl="login.aspx" protection="All" path="/"/>
+        </authentication>
+
+        <authorization>
+            <allow users="?"/>
+        </authorization>
+
+        <!-- Membership Provider -->
+        <membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
+            <providers>
+                <clear/>
+                <add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco.Web"
+                    minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false"
+                    enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed"
+                    allowManuallyChangingPassword="false"/>
+                <add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco.Web"/>
+            </providers>
+        </membership>
+
+        <!-- Role Provider -->
+        <roleManager enabled="true" defaultProvider="UmbracoRoleProvider">
+            <providers>
+                <clear/>
+                <add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider"/>
+            </providers>
+        </roleManager>
+
+    </system.web>
+
+    <system.webServer>
+        <validation validateIntegratedModeConfiguration="false"/>
+
+        <modules runAllManagedModulesForAllRequests="true">
+            <remove name="WebDAVModule"/>
+            <remove name="UmbracoModule"/>
+            <remove name="ScriptModule"/>
+            <remove name="ClientDependencyModule"/>
+            <remove name="FormsAuthentication"/>
+            <remove name="ImageProcessorModule"/>
+
+            <add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web"/>
+            <add name="ScriptModule" preCondition="managedHandler"
+                type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
+            <add name="ClientDependencyModule" type="ClientDependency.Core.Module.ClientDependencyModule, ClientDependency.Core"/>
+            <!-- FormsAuthentication is needed for login/membership to work on homepage (as per http://stackoverflow.com/questions/218057/httpcontext-current-session-is-null-when-routing-requests) -->
+            <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule"/>
+            <add name="ImageProcessorModule" type="ImageProcessor.Web.HttpModules.ImageProcessingModule, ImageProcessor.Web"/>
+        </modules>
+
+        <handlers accessPolicy="Read, Write, Script, Execute">
+            <remove name="WebServiceHandlerFactory-Integrated"/>
+            <remove name="ScriptHandlerFactory"/>
+            <remove name="ScriptHandlerFactoryAppServices"/>
+            <remove name="ScriptResource"/>
+            <remove name="ClientDependency"/>
+            <remove name="MiniProfiler"/>
+            <remove name="ExtensionlessUrlHandler-Integrated-4.0"/>
+            <remove name="OPTIONSVerbHandler"/>
+            <remove name="TRACEVerbHandler"/>
+
+            <add name="ScriptHandlerFactory" verb="*" path="*.asmx" preCondition="integratedMode"
+                type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
+            <add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd" preCondition="integratedMode"
+                type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
+            <add name="ScriptResource" verb="GET,HEAD" path="ScriptResource.axd" preCondition="integratedMode"
+                type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
+            <add verb="*" name="ClientDependency" preCondition="integratedMode" path="DependencyHandler.axd"
+                type="ClientDependency.Core.CompositeFiles.CompositeDependencyHandler, ClientDependency.Core"/>
+            <add name="MiniProfiler" path="mini-profiler-resources/*" verb="*" type="System.Web.Routing.UrlRoutingModule" resourceType="Unspecified"
+                preCondition="integratedMode"/>
+            <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler"
+                preCondition="integratedMode,runtimeVersionv4.0"/>
+        </handlers>
+
+        <staticContent>
+            <remove fileExtension=".air"/>
+            <mimeMap fileExtension=".air" mimeType="application/vnd.adobe.air-application-installer-package+zip"/>
+            <remove fileExtension=".svg"/>
+            <mimeMap fileExtension=".svg" mimeType="image/svg+xml"/>
+            <remove fileExtension=".woff"/>
+            <mimeMap fileExtension=".woff" mimeType="font/woff"/>
+            <remove fileExtension=".woff2"/>
+            <mimeMap fileExtension=".woff2" mimeType="font/woff2"/>
+            <remove fileExtension=".less"/>
+            <mimeMap fileExtension=".less" mimeType="text/css"/>
+            <remove fileExtension=".mp4"/>
+            <mimeMap fileExtension=".mp4" mimeType="video/mp4"/>
+            <remove fileExtension=".json"/>
+            <mimeMap fileExtension=".json" mimeType="application/json"/>
+        </staticContent>
+
+        <!-- Ensure the powered by header is not returned -->
+        <httpProtocol>
+            <customHeaders>
+                <remove name="X-Powered-By"/>
+            </customHeaders>
+        </httpProtocol>
+
+        <!-- Increase the default upload file size limit -->
+        <security>
+            <requestFiltering>
+                <requestLimits maxAllowedContentLength="52428800"/>
+            </requestFiltering>
+        </security>
+
+        <!--
+            If you wish to use IIS rewrite rules, see the documentation here: https://our.umbraco.com/documentation/Reference/Routing/IISRewriteRules
+        -->
+        <!--
+        <rewrite>
+            <rules></rules>
+        </rewrite>
+        -->
+    </system.webServer>
+
+    <runtime>
+        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
+            <dependentAssembly>
+                <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-1.2.5.0" newVersion="1.2.5.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35"/>
+                <bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
+                <bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
+                <bindingRedirect oldVersion="4.0.0.0-4.0.3.0" newVersion="4.0.3.0"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
+                <bindingRedirect oldVersion="4.0.0.0-4.0.1.1" newVersion="4.0.1.1"/>
+            </dependentAssembly>
+            <dependentAssembly>
+                <assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
+                <bindingRedirect oldVersion="4.0.0.0-4.1.4.0" newVersion="4.1.4.0"/>
+            </dependentAssembly>
+        </assemblyBinding>
+    </runtime>
+
+    <location path="umbraco">
+        <system.webServer>
+            <urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false"/>
+        </system.webServer>
+    </location>
+    <location path="App_Plugins">
+        <system.webServer>
+            <urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false"/>
+        </system.webServer>
+    </location>
+
+    <imageProcessor>
+        <security configSource="config\imageprocessor\security.config"/>
+        <caching configSource="config\imageprocessor\cache.config"/>
+        <processing configSource="config\imageprocessor\processing.config"/>
+    </imageProcessor>
+
+    <system.codedom>
+        <compilers>
+            <compiler language="c#;cs;csharp" extension=".cs"
+                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
+                warningLevel="4" compilerOptions="/langversion:7 /nowarn:1659;1699;1701"/>
+            <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
+                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
+                warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+"/>
+        </compilers>
+    </system.codedom>
+
+</configuration>