Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I 2 #38

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

I 2 #38

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
17 changes: 17 additions & 0 deletions .github/workflows/UploadFile_build_and_test_on_main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: UploadFile build_main

on:
push:
branches:
- main
paths:
- "Frends.HTTP.UploadFile/**"
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/build_main.yml@main
with:
workdir: Frends.HTTP.UploadFile
secrets:
badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
18 changes: 18 additions & 0 deletions .github/workflows/UploadFile_build_and_test_on_push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: UploadFile push

on:
push:
branches-ignore:
- main
paths:
- "Frends.HTTP.UploadFile/**"
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/build_test.yml@main
with:
workdir: Frends.HTTP.UploadFile
secrets:
badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }}
test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }}
12 changes: 12 additions & 0 deletions .github/workflows/UploadFile_release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: UploadFile release

on:
workflow_dispatch:

jobs:
build:
uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main
with:
workdir: Frends.HTTP.UploadFile
secrets:
feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }}
5 changes: 5 additions & 0 deletions Frends.HTTP.UploadFile/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## [1.0.0] - 2025-01-09
### Added
- Initial implementation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Pluralsight.Crypto;
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace Frends.HTTP.UploadFile.Tests;

public static class CertificateHandler
{
public static string Handle(string path, string password, bool cleanUp, string thumbPrint)
{
try
{
if (!cleanUp)
{
using CryptContext ctx = new();
ctx.Open();

X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("cn=localhost"),
ValidFrom = DateTime.Today.AddDays(-1),
ValidTo = DateTime.Today.AddMinutes(1),
});
Comment on lines +19 to +27
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Increase certificate validity period for test reliability.

The certificate is valid for only 1 minute which could cause tests to fail if they run longer. Consider increasing the validity period for more reliable testing.

-                        ValidTo = DateTime.Today.AddMinutes(1),
+                        ValidTo = DateTime.Today.AddHours(1),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("cn=localhost"),
ValidFrom = DateTime.Today.AddDays(-1),
ValidTo = DateTime.Today.AddMinutes(1),
});
X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("cn=localhost"),
ValidFrom = DateTime.Today.AddDays(-1),
ValidTo = DateTime.Today.AddHours(1),
});


byte[] certData = cert.Export(X509ContentType.Pfx, password);

using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
}
Comment on lines +16 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Certificate cleanup is missing from test implementations

The test code adds certificates to the CurrentUser store but doesn't clean them up:

  • No certificate removal from store in test cleanup methods
  • Only file system cleanup is implemented
  • Certificates remain in the user's store even after tests complete

Recommendations:

  • Add certificate store cleanup in test cleanup methods using store.Remove(cert)
  • Consider using a dedicated test store location
  • Track certificate thumbprints for cleanup
🔗 Analysis chain

Review security implications of test certificates.

While this is test code, there are several security considerations to be aware of:

  1. Certificates are created with exportable private keys
  2. Certificates are stored in the user's certificate store
  3. The cleanup process is critical to prevent certificate accumulation

Consider adding a finally block to ensure cleanup runs even if tests fail.

Run this script to check for leftover test certificates:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential leftover test certificates
rg -l "ctx\.CreateSelfSignedCertificate" | xargs -I {} rg -A 5 "Name = new X500DistinguishedName\(\"cn=localhost\"\)" {}

Length of output: 734


Script:

#!/bin/bash
# Check for certificate store cleanup mechanisms
rg -B 5 -A 10 "store\.Add\(cert\)" 

Length of output: 3660


Script:

#!/bin/bash
# Look for test cleanup or disposal methods
rg -B 2 -A 10 "public void (Dispose|Cleanup|TearDown)|protected override void Dispose" Frends.HTTP.*Tests/

Length of output: 170


Script:

#!/bin/bash
# Look for test cleanup methods in both projects
echo "=== UploadFile Tests ==="
rg -B 2 -A 10 "public void (Dispose|Cleanup|TearDown)|protected override void Dispose|\[TestCleanup\]" Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/

echo -e "\n=== DownloadFile Tests ==="
rg -B 2 -A 10 "public void (Dispose|Cleanup|TearDown)|protected override void Dispose|\[TestCleanup\]" Frends.HTTP.DownloadFile/Frends.HTTP.DownloadFile.Tests/

Length of output: 1851


File.WriteAllBytes(path, certData);
return cert.Thumbprint;
Comment on lines +14 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input parameter validation.

The method should validate input parameters before proceeding with certificate creation.

Add these checks at the start of the creation block:

             if (!cleanUp)
             {
+                if (string.IsNullOrEmpty(path))
+                    throw new ArgumentNullException(nameof(path));
+                if (string.IsNullOrEmpty(password))
+                    throw new ArgumentNullException(nameof(password));
+
                 using CryptContext ctx = new();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!cleanUp)
{
using CryptContext ctx = new();
ctx.Open();
X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("cn=localhost"),
ValidFrom = DateTime.Today.AddDays(-1),
ValidTo = DateTime.Today.AddMinutes(1),
});
byte[] certData = cert.Export(X509ContentType.Pfx, password);
using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
}
File.WriteAllBytes(path, certData);
return cert.Thumbprint;
if (!cleanUp)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException(nameof(password));
using CryptContext ctx = new();
ctx.Open();
X509Certificate2 cert = ctx.CreateSelfSignedCertificate(
new SelfSignedCertProperties
{
IsPrivateKeyExportable = true,
KeyBitLength = 4096,
Name = new X500DistinguishedName("cn=localhost"),
ValidFrom = DateTime.Today.AddDays(-1),
ValidTo = DateTime.Today.AddMinutes(1),
});
byte[] certData = cert.Export(X509ContentType.Pfx, password);
using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
}
File.WriteAllBytes(path, certData);
return cert.Thumbprint;

}
else
{
using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);

foreach (var cert in col)
store.Remove(cert);

return null;
}
Comment on lines +40 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance cleanup operation safety and logging.

The cleanup operation lacks parameter validation and logging. It could silently fail or remove multiple certificates.

Add validation and logging:

             else
             {
+                if (string.IsNullOrEmpty(thumbPrint))
+                    throw new ArgumentNullException(nameof(thumbPrint));
+
                 using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
                 store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
                 X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);
+
+                if (col.Count == 0)
+                    Console.WriteLine($"Warning: No certificates found with thumbprint {thumbPrint}");
+                else if (col.Count > 1)
+                    Console.WriteLine($"Warning: Found {col.Count} certificates with thumbprint {thumbPrint}");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else
{
using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);
foreach (var cert in col)
store.Remove(cert);
return null;
}
else
{
if (string.IsNullOrEmpty(thumbPrint))
throw new ArgumentNullException(nameof(thumbPrint));
using X509Store store = new(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);
if (col.Count == 0)
Console.WriteLine($"Warning: No certificates found with thumbprint {thumbPrint}");
else if (col.Count > 1)
Console.WriteLine($"Warning: Found {col.Count} certificates with thumbprint {thumbPrint}");
foreach (var cert in col)
store.Remove(cert);
return null;
}

}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
Comment on lines +52 to +55
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve exception handling to preserve stack traces.

The current error handling loses valuable stack trace information and doesn't handle cleanup properly.

Modify the exception handling:

-        catch (Exception ex)
-        {
-            throw new Exception(ex.Message);
-        }
+        catch (Exception ex) when (ex is not ArgumentNullException)
+        {
+            throw new InvalidOperationException("Certificate operation failed", ex);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
catch (Exception ex)
{
throw new Exception(ex.Message);
}
catch (Exception ex) when (ex is not ArgumentNullException)
{
throw new InvalidOperationException("Certificate operation failed", ex);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="HttpMock" Version="2.3.1" />
<PackageReference Include="HttpMock.Verify.NUnit" Version="1.2.0" />
<PackageReference Include="Kayak" Version="0.7.2" />
<PackageReference Include="log4net" Version="2.0.15" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0-preview.23280.1" />
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid preview versions in production code.

The Microsoft.NET.Test.Sdk is using a preview version (17.7.0-preview.23280.1). Consider using the latest stable version.

-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0-preview.23280.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0-preview.23280.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />

<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="Pluralsight.Crypto" Version="1.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Frends.HTTP.UploadFile\Frends.HTTP.UploadFile.csproj" />
</ItemGroup>

</Project>
188 changes: 188 additions & 0 deletions Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.Tests/IntegrationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using Frends.HTTP.UploadFile.Definitions;
using HttpMock;
using HttpMock.Verify.NUnit;
using NUnit.Framework;
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Frends.HTTP.UploadFile.Tests;

[TestFixture]
public class IntegrationTest
{
private IHttpServer _stubHttp;
private static readonly string testFilePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "test_file.txt"));
private static readonly string testCodePageName = "iso-8859-1";
private static readonly string expectedContentType = $"text/plain; charset={testCodePageName}";
private static readonly Header contentTypeHeader = new() { Name = "cONtENT-tYpE", Value = expectedContentType };

private static readonly Input defaultInput = new()
{
Method = Method.POST,
Url = "http://localhost:9191/endpoint",
Headers = new Header[1] { contentTypeHeader },
FilePath = testFilePath
};


[SetUp]
public void Setup()
{
_stubHttp = HttpMockRepository.At("http://localhost:9191");
_stubHttp.Stub(x => x.Post("/endpoint"))
.AsContentType($"text/plain; charset={testCodePageName}")
.Return("foo ���")
.OK();
_stubHttp.Stub(x => x.Post("/wrong-endpoint"))
.AsContentType($"text/plain")
.Return("error occured")
.NotFound();
}

[Test]
public async Task RequestShouldSetEncodingWithContentTypeCharsetIgnoringCase()
{
var utf8ByteArray = File.ReadAllBytes(testFilePath);

var result = await UploadFileTask.UploadFile(defaultInput, new Options(), CancellationToken.None);

var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
var requestHead = request.RequestHead;
var requestBodyByteArray = Encoding.GetEncoding(testCodePageName).GetBytes(request.Body);
var requestContentType = requestHead.Headers["cONTENT-tYpE"];

Assert.That(requestContentType, Is.EqualTo(expectedContentType));
Assert.That(requestBodyByteArray, Is.EqualTo(utf8ByteArray));
}

[Test]
public void ThrowOnErrorResponse()
{
var input = defaultInput;
input.Url = "http://localhost:9191/wrong-endpoint";
var options = new Options
{
ThrowExceptionOnErrorResponse = true
};

Assert.ThrowsAsync<WebException>(() => UploadFileTask.UploadFile(input, options, CancellationToken.None));
}

[Test]
public async Task AddBasicAuthHeader()
{
var options = new Options
{
Authentication = Authentication.Basic,
Username = "foo",
Password = "bar",
};
var encodedValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.Username}:{options.Password}"));

var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);
var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Basic {encodedValue}"));
}

[Test]
public async Task AddOAuthHeader()
{
var options = new Options
{
Authentication = Authentication.OAuth,
Token = "foobar"
};

var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);

var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
Assert.That(request.RequestHead.Headers["Authorization"], Is.EqualTo($"Bearer foobar"));
}

[Test]
public async Task AddInvalidHeader()
{
Header invalidHeader = new() { Name = string.Empty, Value = "bar" };
var input = defaultInput;
input.Headers = new Header[2] { contentTypeHeader, invalidHeader };

var result = await UploadFileTask.UploadFile(input, new Options(), CancellationToken.None);
var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
var isInvalidAdded = request.RequestHead.Headers.Any(x => x.Key == string.Empty);
Assert.IsFalse(isInvalidAdded);
}

[Test]
public async Task AddWindowsIntegratedSecurity()
{
var options = new Options
{
Authentication = Authentication.WindowsIntegratedSecurity
};

var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);

var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
}

[Test]
public async Task AddWindowsAuthentication()
{
var options = new Options
{
Authentication = Authentication.WindowsAuthentication,
Username = "user\\domain"
};

var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);

var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
}

[Test]
public void ThrowWhenAddWindowsAuthenticationWithInvalidUsername()
{
var options = new Options
{
Authentication = Authentication.WindowsAuthentication,
Username = "userWithoutDomain"
};

Assert.ThrowsAsync<ArgumentException>(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None));
}

[Test]
public async Task AddClientCertiface()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the method name typo

The method name AddClientCertiface appears to have a typo. It should likely be AddClientCertificate.

Apply this diff to fix the typo:

- public async Task AddClientCertiface()
+ public async Task AddClientCertificate()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async Task AddClientCertiface()
public async Task AddClientCertificate()

{
string certPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../..", "Test_files", "certwithpk.pfx"));
string certPassword = "password";
var thumprint = CertificateHandler.Handle(certPath, certPassword, false, null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix variable name typo

The variable thumprint is misspelled. It should be thumbprint.

Apply this diff:

- var thumprint = CertificateHandler.Handle(certPath, certPassword, false, null);
+ var thumbprint = CertificateHandler.Handle(certPath, certPassword, false, null);

Also, update the usage in line 169 accordingly:

- CertificateThumbprint = thumprint
+ CertificateThumbprint = thumbprint
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var thumprint = CertificateHandler.Handle(certPath, certPassword, false, null);
var thumbprint = CertificateHandler.Handle(certPath, certPassword, false, null);


var options = new Options
{
Authentication = Authentication.ClientCertificate,
CertificateThumbprint = thumprint
};

var result = await UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None);

var request = _stubHttp.AssertWasCalled(called => called.Post("/endpoint")).LastRequest();
}

[Test]
public void ThrowWhenInvalidClientCertificate()
{
var options = new Options
{
Authentication = Authentication.ClientCertificate,
CertificateThumbprint = "thumbprint"
};

Assert.ThrowsAsync<FileNotFoundException>(() => UploadFileTask.UploadFile(defaultInput, options, CancellationToken.None));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Lorem lipsum
Sumpil merol
37 changes: 37 additions & 0 deletions Frends.HTTP.UploadFile/Frends.HTTP.UploadFile.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile", "Frends.HTTP.UploadFile\Frends.HTTP.UploadFile.csproj", "{35C305C0-8108-4A98-BB1D-AFE5C926239E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frends.HTTP.UploadFile.Tests", "Frends.HTTP.UploadFile.Tests\Frends.HTTP.UploadFile.Tests.csproj", "{8CA92187-8E4F-4414-803B-EC899479022E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{500C38D9-EBDF-49FE-ACFB-D22A5191B8BB}"
ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35C305C0-8108-4A98-BB1D-AFE5C926239E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35C305C0-8108-4A98-BB1D-AFE5C926239E}.Release|Any CPU.Build.0 = Release|Any CPU
{8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CA92187-8E4F-4414-803B-EC899479022E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CA92187-8E4F-4414-803B-EC899479022E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {55BC6629-85C9-48D8-8CA2-B0046AF1AF4B}
EndGlobalSection
EndGlobal
Loading
Loading