Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ updates:
interval: "monthly"
day: "monday"
time: "08:00"

- package-ecosystem: "nuget"
directory: "/client-samples/dotnet/rest"
commit-message:
prefix: "nuget-rest"
schedule:
interval: "monthly"
day: "monday"
time: "08:00"
30 changes: 30 additions & 0 deletions .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: .NET CI

on:
push:
branches: [ "main" ]
paths:
- 'client-samples/dotnet/**'
pull_request:
branches: [ "main" ]
paths:
- 'client-samples/dotnet/**'
jobs:
dotnet-checkout-and-test:
runs-on: windows-latest
defaults:
run:
working-directory: ./client-samples/dotnet/rest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 9.0.x
- name: Restore dependencies
run: dotnet restore api-template && dotnet restore api-template.tests
- name: Build
run: dotnet build api-template --no-restore --configuration Release
- name: Test
run: dotnet test api-template.tests
4 changes: 4 additions & 0 deletions .github/workflows/java-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: Java CI
on:
push:
branches: [ "main" ]
paths:
- 'client-samples/java/**'
pull_request:
branches: [ "main" ]
paths:
- 'client-samples/java/**'
jobs:
java-checkout-and-test:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ name: Python Tests
on:
push:
branches: [ "main" ]
paths:
- "client-samples/python/**"
pull_request:
branches: [ "main" ]
paths:
- "client-samples/python/**"
jobs:
python-checkout-and-test:
runs-on: ubuntu-latest
Expand Down
54 changes: 54 additions & 0 deletions client-samples/dotnet/rest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## A streamlined .gitignore for modern .NET projects
## including temporary files, build results, and
## files generated by popular .NET tools. If you are
## developing with Visual Studio, the VS .gitignore
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
## has more thorough IDE-specific entries.
##
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# ASP.NET Scaffolding
ScaffoldingReadMe.txt

# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg

# Others
~$*
*~
CodeCoverage/

# MSBuild Binary and Structured Log
*.binlog

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
72 changes: 72 additions & 0 deletions client-samples/dotnet/rest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# .NET Rest Client Example Project
This project is a starting template for integrating with REST APIs on the API Platform.

It requires some simple security configuration to enable you to authenticate to the platform.

## Requirements
- .NET 9.0.x
- A client application on Morgan Stanley Azure AD tenant. Please talk to your contact at Morgan Stanley to set this up.
- A self-signed public/private key pair. Please see the client setup guide for instructions to generate this.

## Configuration
Set the following values in the ClientCredentials section in [appsettings.json](./api-template/appsettings.json).

| Property Name | Description |
|----------------------|-----------------------------------------------------------------------------------------|
| `ClientId` | The client id that will be sent to you from your Morgan Stanley contact |
| `PrivateKeyFile` | The path to the private_key.pem that has been created |
| `PublicKeyFile` | The path to the public_key.cer that was created and sent to your Morgan Stanley contact |
| `Scopes` | The scope/s that will be sent to you from your Morgan Stanley contact |
| `Tenant` | The tenant that will be sent to you from your Morgan Stanley contact |
| `Url` | The URL of the Morgan Stanley API endpoint you are connecting to |

### Proxies
If you would like to route your API call through a Proxy, then add the `ProxySettings` section to [appsettings.json](./api-template/appsettings.json). For example:

```json
{
"ClientCredentials": {
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
...
},
"ProxySettings": {
"ProxyHost": "proxy-host",
"ProxyPort": "8080"
}
}
```

### Running the Application
Restore dependencies:

```shell
dotnet restore api-template
dotnet restore api-template.tests
```

Run the application:

```shell
dotnet run --project api-template
```

Build the application:

```shell
dotnet build api-template
```

### Testing the Application

Run unit tests:

```shell
dotnet test api-template.tests
```

# Legal

Morgan Stanley makes this available to you under the Apache License, Version 2.0 (the "License"). You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
See the NOTICE file distributed with this work for additional information regarding copyright ownership.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>api_template.tests</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="WireMock.Net" Version="1.8.5" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<None Update="test-appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<None Update="test-appsettings-with-proxy.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\api-template\api-template.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using WireMock.Server;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;

namespace ApiTemplate.Tests.Services;

public class TestApiService
{
[Fact]
public async Task TestGetResponseAsync()
{
// Arrange: Start the mock server
var server = WireMockServer.Start();

// Define the mock server's URL
string endpoint = "/api/test";
var mockUrl = $"{server.Urls[0]}{endpoint}";

// Configure the mock server to respond to a GET request
server.Given(
Request.Create()
.WithPath(endpoint)
.UsingGet()
).RespondWith(
Response.Create()
.WithStatusCode(200)
.WithBody("Mock Response")
);

// Act: Call the ApiService.GetResponse method
var token = "mock-token";
var proxy = string.Empty; // No proxy for this test
var response = ApiService.GetResponse(mockUrl, token, proxy);

// Assert: Verify the response
Assert.NotNull(response);
Assert.Equal(200, (int)response.StatusCode);
var responseBody = await response.Content.ReadAsStringAsync();
Assert.Equal("Mock Response", responseBody);

// Verify the mock server received the request
var logEntries = server.LogEntries;
Assert.Single(logEntries);
Assert.Contains(logEntries, log => log.RequestMessage.Path == endpoint && log.RequestMessage.Method == "GET");

// Assert: Verify the request had the expected Authorization header
Assert.Contains(logEntries, log =>
log.RequestMessage.Headers.ContainsKey("Authorization") &&

Check warning on line 48 in client-samples/dotnet/rest/api-template.tests/services/TestApiService.cs

View workflow job for this annotation

GitHub Actions / dotnet-checkout-and-test

Dereference of a possibly null reference.
log.RequestMessage.Headers["Authorization"].Contains($"Bearer {token}")
);

// Stop the server
server.Stop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using NSubstitute;

namespace api_template.tests.services;

public class TestAuthTokenService
{
private readonly ClientCredentialSettings _clientCredentialSettings;
private readonly AuthTokenService _authTokenServiceMock;

public TestAuthTokenService()
{
_clientCredentialSettings = new ClientCredentialSettings
{
ClientId = "your-client-id",
PrivateKeyFile = "path/to/privateKey.pem",
PublicKeyFile = "path/to/publicKey.pem",
Scopes = new() { "scope1", "scope2" },
Tenant = "your-tenant-id",
Url = "https://your-auth-url.com"
};
_authTokenServiceMock = Substitute.For<AuthTokenService>(_clientCredentialSettings);
}

[Fact]
public void GetsCorrectAuthority()
{
// Arrange
var expectedAuthority = $"https://login.microsoftonline.com/{_clientCredentialSettings.Tenant}";

// Act
var authority = _authTokenServiceMock.GetAuthority(_clientCredentialSettings.Tenant);

// Assert
Assert.Equal(expectedAuthority, authority);
}

[Fact]
public void TestGetsToken()
{
// Arrange
string mockToken = "mock token";

var mockConfidentialClient = Substitute.For<IConfidentialClientApplication>();

_authTokenServiceMock.When(mock => mock.GetCertificate(
Arg.Any<string>(),
Arg.Any<string>()
)).DoNotCallBase();

_authTokenServiceMock.When(mock => mock.CreateConfidentialClient(
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<X509Certificate2>()
)).DoNotCallBase();

_authTokenServiceMock.GetToken(
Arg.Any<IConfidentialClientApplication>()
).Returns(mockToken);

// Act
var token = _authTokenServiceMock.GetAuthToken();

// Assert
_authTokenServiceMock.Received(1).GetCertificate(
_clientCredentialSettings.PublicKeyFile,
_clientCredentialSettings.PrivateKeyFile
);

_authTokenServiceMock.Received(1).CreateConfidentialClient(
_clientCredentialSettings.ClientId,
$"https://login.microsoftonline.com/{_clientCredentialSettings.Tenant}",
Arg.Any<X509Certificate2>()
);

_ = _authTokenServiceMock.Received(1).GetToken(
Arg.Any<IConfidentialClientApplication>()
);

Assert.NotNull(token);
Assert.Equal(mockToken, token);
}
}
Loading