diff --git a/src/AzureExtension/Assets/AzureExtensionDark.png b/src/AzureExtension/Assets/AzureExtensionDark.png new file mode 100644 index 00000000..fa5c1344 Binary files /dev/null and b/src/AzureExtension/Assets/AzureExtensionDark.png differ diff --git a/src/AzureExtension/Client/AzureUri.cs b/src/AzureExtension/Client/AzureUri.cs index 67a9d89f..d1e49cab 100644 --- a/src/AzureExtension/Client/AzureUri.cs +++ b/src/AzureExtension/Client/AzureUri.cs @@ -143,7 +143,7 @@ private AzureHostType InitializeAzureHostType() { return AzureHostType.NotHosted; } - else if (Uri.Host.Equals("dev.azure.com", StringComparison.OrdinalIgnoreCase)) + else if (Uri.Host.EndsWith("dev.azure.com", StringComparison.OrdinalIgnoreCase)) { return AzureHostType.Modern; } @@ -211,15 +211,26 @@ private string InitializeOrganization() try { - return HostType switch + switch (HostType) { - // https://dev.azure.com/{organization} (modern) - AzureHostType.Modern => Uri.Segments[1].Replace("/", string.Empty), - - // https://{organization}.visualstudio.com (legacy) - AzureHostType.Legacy => Uri.Host.Replace(".visualstudio.com", string.Empty, StringComparison.OrdinalIgnoreCase), - _ => string.Empty, - }; + case AzureHostType.Modern: + return Uri.Segments[1].Replace("/", string.Empty); + + case AzureHostType.Legacy: + // Legacy format can have "vssps" in the uri, which we need to ignore for + // extracting the url + if (Uri.Host.EndsWith(".vssps.visualstudio.com", StringComparison.OrdinalIgnoreCase)) + { + return Uri.Host.Replace(".vssps.visualstudio.com", string.Empty, StringComparison.OrdinalIgnoreCase); + } + else + { + return Uri.Host.Replace(".visualstudio.com", string.Empty, StringComparison.OrdinalIgnoreCase); + } + + default: + return string.Empty; + } } catch (Exception e) { @@ -423,6 +434,9 @@ private Uri InitializeConnection() case AzureHostType.Legacy: // Legacy format is just the authority, as the organization is in the subdomain. + // Note that Authority will not contain the port number unless the port + // number differs from the default port. So if Port 443 is specified, Authority will + // not list it as that is the default port for the https scheme. var legacyUriString = Uri.Scheme + "://" + Uri.Authority; legacyUriString = legacyUriString.TrimEnd('/') + '/'; if (!Uri.TryCreate(legacyUriString, UriKind.Absolute, out newUri)) diff --git a/src/AzureExtension/Providers/RepositoryProvider.cs b/src/AzureExtension/Providers/RepositoryProvider.cs index fecc5308..bbb2b1da 100644 --- a/src/AzureExtension/Providers/RepositoryProvider.cs +++ b/src/AzureExtension/Providers/RepositoryProvider.cs @@ -34,7 +34,7 @@ public RepositoryProvider(IRandomAccessStreamReference icon) public RepositoryProvider() { - Icon = RandomAccessStreamReference.CreateFromUri(new Uri("https://www.GitHub.com/microsoft/devhome")); + Icon = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///AzureExtension/Assets/AzureExtensionDark.png")); } public IAsyncOperation IsUriSupportedAsync(Uri uri) diff --git a/src/AzureExtension/Strings/en-US/Resources.resw b/src/AzureExtension/Strings/en-US/Resources.resw index ca3bb99f..c0001dd9 100644 --- a/src/AzureExtension/Strings/en-US/Resources.resw +++ b/src/AzureExtension/Strings/en-US/Resources.resw @@ -162,7 +162,7 @@ The name of our application - Azure + Azure DevOps Shown in various places, is Azure even localized as a product name? diff --git a/test/AzureExtension/Widgets/Validation.cs b/test/AzureExtension/Widgets/Validation.cs index 9ca06e35..21378e13 100644 --- a/test/AzureExtension/Widgets/Validation.cs +++ b/test/AzureExtension/Widgets/Validation.cs @@ -13,6 +13,7 @@ public enum AzureUriType Query, Repository, Unknown, + SignIn, Garbage, // Anything that is expected to fail creation by a normal Uri. } @@ -92,6 +93,43 @@ public void AzureUriValidation() Tuple.Create("https://organization.visualstudio.com/collection/project/_git/repository", AzureUriType.Repository, true), Tuple.Create("https://organization.visualstudio.com/collection/project/_git/repository/", AzureUriType.Repository, true), Tuple.Create("https://organization.visualstudio.com/collection/project/_git/repository/some/other/stuff", AzureUriType.Repository, true), + + // Azure DevOps services SignIn Uris + Tuple.Create("https://app.vssps.visualstudio.com/", AzureUriType.SignIn, true), + Tuple.Create("https://app.vssps.visualstudio.com/with/extra/stuff", AzureUriType.SignIn, true), + Tuple.Create("https://app.vssps.visualstudio.com:443/", AzureUriType.SignIn, true), + Tuple.Create("https://app.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, true), + Tuple.Create("https://organization.vssps.visualstudio.com/", AzureUriType.SignIn, true), + Tuple.Create("https://organization.vssps.visualstudio.com/with/extra/stuff", AzureUriType.SignIn, true), + Tuple.Create("https://organization.vssps.visualstudio.com:443/", AzureUriType.SignIn, true), + Tuple.Create("https://organization.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, true), + + // Azure DevOps services SignIn Uris, modern format. + // Note port 443 is the default port for https, so Uri objects remove it. + Tuple.Create("https://vssps.dev.azure.com/organization", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com/organization/", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com:443/organization", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com:443/organization/", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com/organization/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com/organization/with/extra/stuff/", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com:443/organization/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://vssps.dev.azure.com:443/organization/with/extra/stuff/", AzureUriType.SignIn, false), + + // Azure Devops SignIn Uris, legacy format. + // Note port 443 is the default port for https, so Uri objects remove it. + Tuple.Create("https://app.vssps.visualstudio.com/", AzureUriType.SignIn, false), + Tuple.Create("https://app.vssps.visualstudio.com:443/", AzureUriType.SignIn, false), + Tuple.Create("https://app.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com/", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com:443/", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://app.vssps.visualstudio.com:443/", AzureUriType.SignIn, false), + Tuple.Create("https://app.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com/", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com/with/extra/stuff", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com:443/", AzureUriType.SignIn, false), + Tuple.Create("https://organization.vssps.visualstudio.com:443/with/extra/stuff", AzureUriType.SignIn, false), }; var testUrisInvalid = new List> @@ -122,14 +160,21 @@ public void AzureUriValidation() Assert.AreEqual(azureUri.Uri, uri); } + // All Valid inputs should be valid. + Assert.IsTrue(azureUri.IsValid); + + // We only support hosted for now. Assert.IsTrue(azureUri.IsHosted); - var org = azureUri.Organization; - var project = azureUri.Project; - var repository = azureUri.Repository; - var query = azureUri.Query; - TestContext?.WriteLine($"Org: {org} Project: {project} Repository: {repository} Query: {query}"); - Assert.AreEqual("organization", org); - Assert.AreEqual("project", project); + TestContext?.WriteLine($"Org: {azureUri.Organization} Project: {azureUri.Project} Repository: {azureUri.Repository} Query: {azureUri.Query}"); + + if (uriTuple.Item2 != AzureUriType.SignIn) + { + // Signin Uris may not have project. + // Signin Uris can also have "app" as the organization. + // Organization will be validated as part of that type. + Assert.AreEqual("organization", azureUri.Organization); + Assert.AreEqual("project", azureUri.Project); + } // Validate the constructor is identical if it is created from a Uri instead of a string. if (uriTuple.Item2 != AzureUriType.Garbage) @@ -139,44 +184,87 @@ public void AzureUriValidation() Assert.AreEqual(azureUri.ToString(), uri.ToString()); Assert.AreEqual(azureUri.OriginalString, uri.OriginalString); Assert.AreEqual(azureUri.Uri, uri); + + var connectionUri = azureUri.Connection; + Assert.IsNotNull(connectionUri); + TestContext?.WriteLine($"Connection: {connectionUri}"); + + // All Https Uris have port 443 by default. + // We require https uri, therefore we must have port 443. + Assert.IsTrue(connectionUri.Port == 443); + + // A properly constructed connectionUri will be contained within the full Uri. + // We use a separate standard Uri to compare so we get expected Uri behavior w/r/t + // the Authority, which will not contain the port if it is the scheme's default. + + // The only modification is to enforce a trailing / which may or may not be there, + // but the trailing slash is part of the Authority and we normalize to that. + // We are testing for functional equivalence. + var normalizedOriginal = uri.ToString().Trim('/') + '/'; + Assert.IsTrue(normalizedOriginal.StartsWith(connectionUri.ToString(), StringComparison.OrdinalIgnoreCase)); } // Verify Query Uris have expected values. if (uriTuple.Item2 == AzureUriType.Query) { Assert.IsTrue(azureUri.IsQuery); - Assert.AreEqual("12345678-1234-1234-1234-1234567890ab", query); + Assert.AreEqual("12345678-1234-1234-1234-1234567890ab", azureUri.Query); } else { Assert.IsFalse(azureUri.IsQuery); - Assert.AreEqual(string.Empty, query); + Assert.AreEqual(string.Empty, azureUri.Query); } // Verify repository Uris have expected values. if (uriTuple.Item2 == AzureUriType.Repository) { Assert.IsTrue(azureUri.IsRepository); - Assert.AreEqual("repository", repository); + Assert.AreEqual("repository", azureUri.Repository); } else { Assert.IsFalse(azureUri.IsRepository); - Assert.AreEqual(string.Empty, repository); + Assert.AreEqual(string.Empty, azureUri.Repository); } - var connectionUri = azureUri.Connection; - Assert.IsNotNull(connectionUri); - TestContext?.WriteLine($"Connection: {connectionUri}"); - - // Verify connection URI remains in the original format. Legacy URIs should use legacy connections. - if (azureUri.HostType == AzureHostType.Legacy) + if (uriTuple.Item2 == AzureUriType.SignIn) { - Assert.AreEqual("https://organization.visualstudio.com/", connectionUri.ToString()); - } - else - { - Assert.AreEqual("https://dev.azure.com/organization/", connectionUri.ToString()); + if (azureUri.Organization.Equals("app", StringComparison.OrdinalIgnoreCase)) + { + // App is valid if it is parsed as an organization, but only + // on legacy Uris. This directly validates that + // app.vssps.visualstudio.com is a valid input uri. + Assert.IsTrue(azureUri.HostType == AzureHostType.Legacy); + } + else + { + // All other matches are assumed to be organization names. + // This ensures we do not treat the "vssps" part as being part of the org name. + Assert.AreEqual("organization", azureUri.Organization); + } + + // Test for equivalence of Connection Uris when the base Uri and the Connection + // should be equivalent. + if (azureUri.HostType == AzureHostType.Modern) + { + // Modern hosts have bare minimum Org requirement. + if (azureUri.Uri.Segments.Length == 2) + { + // Exact equivalence may not be true depending on trailing slash in the + // original Uri, but these are still functionally equivalent. + var normalizedOriginal = azureUri.Uri.ToString().Trim('/') + '/'; + Assert.IsTrue(normalizedOriginal.Equals(azureUri.Connection.ToString(), StringComparison.OrdinalIgnoreCase)); + } + } + else + { + if (azureUri.Uri.Segments.Length < 2) + { + var normalizedOriginal = azureUri.Uri.ToString().Trim('/') + '/'; + Assert.IsTrue(normalizedOriginal.Equals(azureUri.Connection.ToString(), StringComparison.OrdinalIgnoreCase)); + } + } } TestContext?.WriteLine($"Valid: {uriTuple.Item1}");