diff --git a/backend/src/Designer/Services/Implementation/SourceControlSI.cs b/backend/src/Designer/Services/Implementation/SourceControlSI.cs index 32ab1639f7b..7f28fa9f71a 100644 --- a/backend/src/Designer/Services/Implementation/SourceControlSI.cs +++ b/backend/src/Designer/Services/Implementation/SourceControlSI.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Commit = LibGit2Sharp.Commit; namespace Altinn.Studio.Designer.Services.Implementation { @@ -56,8 +57,12 @@ public string CloneRemoteRepository(string org, string repository) { string remoteRepo = FindRemoteRepoLocation(org, repository); CloneOptions cloneOptions = new(); - cloneOptions.CredentialsProvider = (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; - return LibGit2Sharp.Repository.Clone(remoteRepo, FindLocalRepoLocation(org, repository), cloneOptions); + cloneOptions.CredentialsProvider = CredentialsProvider(); + string localPath = FindLocalRepoLocation(org, repository); + string cloneResult = LibGit2Sharp.Repository.Clone(remoteRepo, localPath, cloneOptions); + + FetchGitNotes(localPath); + return cloneResult; } /// @@ -65,14 +70,16 @@ public string CloneRemoteRepository(string org, string repository, string destin { string remoteRepo = FindRemoteRepoLocation(org, repository); CloneOptions cloneOptions = new(); - cloneOptions.CredentialsProvider = (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; + cloneOptions.CredentialsProvider = CredentialsProvider(); if (!string.IsNullOrEmpty(branchName)) { cloneOptions.BranchName = branchName; } - return LibGit2Sharp.Repository.Clone(remoteRepo, destinationPath, cloneOptions); + string cloneResult = LibGit2Sharp.Repository.Clone(remoteRepo, destinationPath, cloneOptions); + FetchGitNotes(destinationPath); + return cloneResult; } /// @@ -89,8 +96,7 @@ public RepoStatus PullRemoteChanges(string org, string repository) }, }; pullOptions.FetchOptions = new FetchOptions(); - pullOptions.FetchOptions.CredentialsProvider = (url, user, cred) => - new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; + pullOptions.FetchOptions.CredentialsProvider = CredentialsProvider(); try { @@ -130,8 +136,7 @@ public void FetchRemoteChanges(string org, string repository) using (var repo = new LibGit2Sharp.Repository(FindLocalRepoLocation(org, repository))) { FetchOptions fetchOptions = new(); - fetchOptions.CredentialsProvider = (url, user, cred) => - new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; + fetchOptions.CredentialsProvider = CredentialsProvider(); foreach (Remote remote in repo?.Network?.Remotes) { @@ -166,31 +171,29 @@ public async Task Push(string org, string repository) { bool pushSuccess = true; string localServiceRepoFolder = _settings.GetServicePath(org, repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext)); - using (LibGit2Sharp.Repository repo = new(localServiceRepoFolder)) + using LibGit2Sharp.Repository repo = new(localServiceRepoFolder); + string remoteUrl = FindRemoteRepoLocation(org, repository); + Remote remote = repo.Network.Remotes["origin"]; + + if (!remote.PushUrl.Equals(remoteUrl)) { - string remoteUrl = FindRemoteRepoLocation(org, repository); - Remote remote = repo.Network.Remotes["origin"]; + // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. + // Requires adminstrator access to update files. + repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); + } - if (!remote.PushUrl.Equals(remoteUrl)) + PushOptions options = new() + { + OnPushStatusError = pushError => { - // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. - // Requires adminstrator access to update files. - repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); + _logger.LogError("Push error: {0}", pushError.Message); + pushSuccess = false; } + }; + options.CredentialsProvider = CredentialsProvider(); - PushOptions options = new() - { - OnPushStatusError = pushError => - { - _logger.LogError("Push error: {0}", pushError.Message); - pushSuccess = false; - } - }; - options.CredentialsProvider = (url, user, cred) => - new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; - - repo.Network.Push(remote, @"refs/heads/master", options); - } + repo.Network.Push(remote, @"refs/heads/master", options); + repo.Network.Push(remote, "refs/notes/commits", options); return await Task.FromResult(pushSuccess); } @@ -201,24 +204,31 @@ public async Task Push(string org, string repository) /// Information about the commit public void Commit(CommitInfo commitInfo) { - string localServiceRepoFolder = _settings.GetServicePath(commitInfo.Org, commitInfo.Repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext)); - using (LibGit2Sharp.Repository repo = new(localServiceRepoFolder)) + CommitAndAddStudioNote(commitInfo.Org, commitInfo.Repository, commitInfo.Message); + } + + private void CommitAndAddStudioNote(string org, string repository, string message) + { + string localServiceRepoFolder = _settings.GetServicePath(org, repository, AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext)); + using LibGit2Sharp.Repository repo = new LibGit2Sharp.Repository(localServiceRepoFolder); + string remoteUrl = FindRemoteRepoLocation(org, repository); + Remote remote = repo.Network.Remotes["origin"]; + + if (!remote.PushUrl.Equals(remoteUrl)) { - string remoteUrl = FindRemoteRepoLocation(commitInfo.Org, commitInfo.Repository); - Remote remote = repo.Network.Remotes["origin"]; + // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. + // Requires adminstrator access to update files. + repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); + } - if (!remote.PushUrl.Equals(remoteUrl)) - { - // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. - // Requires adminstrator access to update files. - repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); - } + Commands.Stage(repo, "*"); - Commands.Stage(repo, "*"); + LibGit2Sharp.Signature signature = GetDeveloperSignature(); + var commit = repo.Commit(message, signature, signature); + + var notes = repo.Notes; + notes.Add(commit.Id, "studio-commit", signature, signature, notes.DefaultNamespace); - LibGit2Sharp.Signature signature = GetDeveloperSignature(); - repo.Commit(commitInfo.Message, signature, signature); - } } /// @@ -463,39 +473,41 @@ public void VerifyCloneExists(string org, string repository) private void CommitAndPushToBranch(string org, string repository, string branchName, string localPath, string message) { - using (LibGit2Sharp.Repository repo = new(localPath)) + using LibGit2Sharp.Repository repo = new(localPath); + // Restrict users from empty commit + if (repo.RetrieveStatus().IsDirty) { - // Restrict users from empty commit - if (repo.RetrieveStatus().IsDirty) - { - string remoteUrl = FindRemoteRepoLocation(org, repository); - Remote remote = repo.Network.Remotes["origin"]; + string remoteUrl = FindRemoteRepoLocation(org, repository); + Remote remote = repo.Network.Remotes["origin"]; - if (!remote.PushUrl.Equals(remoteUrl)) - { - // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. - // Requires adminstrator access to update files. - repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); - } + if (!remote.PushUrl.Equals(remoteUrl)) + { + // This is relevant when we switch beteen running designer in local or in docker. The remote URL changes. + // Requires adminstrator access to update files. + repo.Network.Remotes.Update("origin", r => r.Url = remoteUrl); + } - Commands.Stage(repo, "*"); + Commands.Stage(repo, "*"); - LibGit2Sharp.Signature signature = GetDeveloperSignature(); - repo.Commit(message, signature, signature); + LibGit2Sharp.Signature signature = GetDeveloperSignature(); + var commit = repo.Commit(message, signature, signature); + var notes = repo.Notes; + notes.Add(commit.Id, "studio-commit", signature, signature, notes.DefaultNamespace); - PushOptions options = new(); - options.CredentialsProvider = (url, user, cred) => - new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; + PushOptions options = new(); + options.CredentialsProvider = CredentialsProvider(); - if (branchName == "master") - { - repo.Network.Push(remote, @"refs/heads/master", options); - return; - } + if (branchName == "master") + { + repo.Network.Push(remote, @"refs/heads/master", options); + repo.Network.Push(remote, "refs/notes/commits", options); - Branch b = repo.Branches[branchName]; - repo.Network.Push(b, options); + return; } + + Branch b = repo.Branches[branchName]; + repo.Network.Push(b, options); + repo.Network.Push(remote, "refs/notes/commits", options); } } @@ -624,5 +636,17 @@ private LibGit2Sharp.Signature GetDeveloperSignature() { return new LibGit2Sharp.Signature(AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext), "@jugglingnutcase", DateTime.Now); } + + private LibGit2Sharp.Handlers.CredentialsHandler CredentialsProvider() => (url, user, cred) => new UsernamePasswordCredentials { Username = GetAppToken(), Password = string.Empty }; + + private void FetchGitNotes(string localRepositoryPath) + { + using var repo = new LibGit2Sharp.Repository(localRepositoryPath); + var options = new FetchOptions() + { + CredentialsProvider = CredentialsProvider() + }; + Commands.Fetch(repo, "origin", new List { "refs/notes/*:refs/notes/*" }, options, "fetch notes"); + } } } diff --git a/backend/tests/Designer.Tests/GiteaIntegrationTests/GiteaIntegrationTestsBase.cs b/backend/tests/Designer.Tests/GiteaIntegrationTests/GiteaIntegrationTestsBase.cs index ae3f5cab693..072e263c12f 100644 --- a/backend/tests/Designer.Tests/GiteaIntegrationTests/GiteaIntegrationTestsBase.cs +++ b/backend/tests/Designer.Tests/GiteaIntegrationTests/GiteaIntegrationTestsBase.cs @@ -8,7 +8,6 @@ using Designer.Tests.Fixtures; using DotNet.Testcontainers.Builders; using FluentAssertions; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing.Handlers; using Microsoft.AspNetCore.TestHost; @@ -28,6 +27,10 @@ public abstract class GiteaIntegrationTestsBase : ApiTestsBase< private CookieContainer CookieContainer { get; } = new CookieContainer(); + /// On some systems path too long error occurs if repo is nested deep in file system. + protected override string TestRepositoriesLocation => + Path.Combine(Path.GetTempPath(), "altinn", "tests", "repos"); + /// /// Used when performing chained calls to designer api /// @@ -50,14 +53,17 @@ protected override void Dispose(bool disposing) DeleteDirectoryIfExists(CreatedFolderPath); } - private static void DeleteDirectoryIfExists(string directoryPath) + protected static void DeleteDirectoryIfExists(string directoryPath) { if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath)) { return; } - var directory = new DirectoryInfo(directoryPath) { Attributes = FileAttributes.Normal }; + var directory = new DirectoryInfo(directoryPath) + { + Attributes = FileAttributes.Normal + }; foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories)) { @@ -131,5 +137,31 @@ protected async Task CreateAppUsingDesigner(string org, string repoName) response.StatusCode.Should().Be(HttpStatusCode.Created); InvalidateAllCookies(); } + + protected static string GetCommitInfoJson(string text, string org, string repository) => + @$"{{ + ""message"": ""{text}"", + ""org"": ""{org}"", + ""repository"": ""{repository}"" + }}"; + + protected static string GenerateCommitJsonPayload(string text, string message) => + @$"{{ + ""author"": {{ + ""email"": ""{GiteaConstants.AdminEmail}"", + ""name"": ""{GiteaConstants.AdminUser}"" + }}, + ""committer"": {{ + ""email"": ""{GiteaConstants.AdminEmail}"", + ""name"": ""{GiteaConstants.AdminUser}"" + }}, + ""content"": ""{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}"", + ""dates"": {{ + ""author"": ""{DateTime.Now:O}"", + ""committer"": ""{DateTime.Now:O}"" + }}, + ""message"": ""{message}"", + ""signoff"": true + }}"; } } diff --git a/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/CopyAppGiteaIntegrationTests.cs b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/CopyAppGiteaIntegrationTests.cs new file mode 100644 index 00000000000..85859106ea4 --- /dev/null +++ b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/CopyAppGiteaIntegrationTests.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Designer.Tests.Fixtures; +using Designer.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Polly; +using Polly.Retry; +using Xunit; + +namespace Designer.Tests.GiteaIntegrationTests.RepositoryController +{ + public class CopyAppGiteaIntegrationTests : GiteaIntegrationTestsBase, IClassFixture> + { + + private string CopyRepoName { get; set; } + + public CopyAppGiteaIntegrationTests(WebApplicationFactory factory, GiteaFixture giteaFixture) : base(factory, giteaFixture) + { + } + + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task Copy_Repo_Should_Return_Created(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + + CopyRepoName = TestDataHelper.GenerateTestRepoName("-gitea-copy"); + + // Copy app + using HttpResponseMessage commitResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/copy-app?sourceRepository={targetRepo}&targetRepository={CopyRepoName}", null); + commitResponse.StatusCode.Should().Be(HttpStatusCode.Created); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + { + return; + } + if (string.IsNullOrEmpty(CopyRepoName)) + { + return; + } + + string copyRepoPath = Path.Combine(TestRepositoriesLocation, "testUser", "ttd", CopyRepoName); + DeleteDirectoryIfExists(copyRepoPath); + } + } +} diff --git a/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/GitNotesGiteaIntegrationTests.cs b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/GitNotesGiteaIntegrationTests.cs new file mode 100644 index 00000000000..53f68f77c98 --- /dev/null +++ b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/GitNotesGiteaIntegrationTests.cs @@ -0,0 +1,174 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Models; +using Designer.Tests.Fixtures; +using Designer.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.GiteaIntegrationTests.RepositoryController +{ + public class GitNotesGiteaIntegrationTests : GiteaIntegrationTestsBase, IClassFixture> + { + + public GitNotesGiteaIntegrationTests(WebApplicationFactory factory, GiteaFixture giteaFixture) : base(factory, giteaFixture) + { + } + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task Commit_AndPush_Separate_Should_Create_GitNote(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + + // Commit and push separately + InvalidateAllCookies(); + await File.WriteAllTextAsync($"{CreatedFolderPath}/test3.txt", "I am a new file"); + using var commitContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit", commitContent); + commitResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + InvalidateAllCookies(); + using HttpResponseMessage pushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/push", null); + pushResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + } + + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task Commit_AndPush_AndContents_Should_Create_GitNote(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + + // Add a file to local repo and try to push with designer + await File.WriteAllTextAsync($"{CreatedFolderPath}/test.txt", "I am a new file"); + + InvalidateAllCookies(); + using var commitAndPushContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitAndPushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); + commitAndPushResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + } + + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task Commit_AndPush_AndContents_WorksAfterResetOfRepo(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + + // Add a file to local repo and try to push with designer + await File.WriteAllTextAsync($"{CreatedFolderPath}/test.txt", "I am a new file"); + + InvalidateAllCookies(); + using var commitAndPushContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitAndPushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); + commitAndPushResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + + // reset repo + InvalidateAllCookies(); + using HttpResponseMessage resetResponse = await HttpClient.GetAsync($"designer/api/repos/repo/{org}/{targetRepo}/reset"); + resetResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + // this ensures local clone + InvalidateAllCookies(); + using HttpResponseMessage appDevelopmentIndes = await HttpClient.GetAsync($"editor/{org}/{targetRepo}"); + appDevelopmentIndes.StatusCode.Should().Be(HttpStatusCode.OK); + + // Try to create a new commit + await File.WriteAllTextAsync($"{CreatedFolderPath}/newFile.txt", "I am a new file"); + + InvalidateAllCookies(); + using var commitAndPushContent2 = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitAndPushResponse2 = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); + commitAndPushResponse2.StatusCode.Should().Be(HttpStatusCode.OK); + + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + } + + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task LocalAndStudioDevelopment_PullLocalCommitFirst_BehaveAsExpected(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + + // Create a file using gitea client + using var createFileContent = new StringContent(GenerateCommitJsonPayload("I am a new file created in gitea", "test gitea commit"), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage createFileResponse = await GiteaFixture.GiteaClient.Value.PostAsync($"repos/{org}/{targetRepo}/contents/test2.txt", createFileContent); + createFileResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Try pull file with designer endpoint + InvalidateAllCookies(); + using HttpResponseMessage pullResponse = await HttpClient.GetAsync($"designer/api/repos/repo/{org}/{targetRepo}/pull"); + pullResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + // Add a new file and try to push with designer + await File.WriteAllTextAsync($"{CreatedFolderPath}/test3.txt", "I am a new file created directly with gitea"); + InvalidateAllCookies(); + using var commitAndPushContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitAndPushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); + commitAndPushResponse.StatusCode.Should().Be(HttpStatusCode.OK); + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + } + + [Theory] + [Trait("Category", "GiteaIntegrationTest")] + [InlineData(GiteaConstants.TestOrgUsername)] + public async Task LocalAndStudioDevelopment_BeginEditAndPullLocalCommit(string org) + { + string targetRepo = TestDataHelper.GenerateTestRepoName("-gitea"); + await CreateAppUsingDesigner(org, targetRepo); + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + + // Create a file using gitea client + using var createFileContent = new StringContent(GenerateCommitJsonPayload("I am a new file created in gitea", "test gitea commit"), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage createFileResponse = await GiteaFixture.GiteaClient.Value.PostAsync($"repos/{org}/{targetRepo}/contents/test2.txt", createFileContent); + createFileResponse.StatusCode.Should().Be(HttpStatusCode.Created); + + // Add a new file and try to push with designer + await File.WriteAllTextAsync($"{CreatedFolderPath}/test3.txt", "I am a new file created directly with gitea"); + + // Try pull file with designer endpoint + InvalidateAllCookies(); + using HttpResponseMessage pullResponse = await HttpClient.GetAsync($"designer/api/repos/repo/{org}/{targetRepo}/pull"); + pullResponse.StatusCode.Should().Be(HttpStatusCode.OK); + + + InvalidateAllCookies(); + using var commitAndPushContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); + using HttpResponseMessage commitAndPushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); + commitAndPushResponse.StatusCode.Should().Be(HttpStatusCode.OK); + await VerifyStudioNoteAddedToLatestCommit(org, targetRepo); + } + + private async Task VerifyStudioNoteAddedToLatestCommit(string org, string targetRepo) + { + InvalidateAllCookies(); + // Check if note is added to a commit + using HttpResponseMessage getCommitResponse = await HttpClient.GetAsync($"designer/api/repos/repo/{org}/{targetRepo}/latest-commit"); + Commit commit = await getCommitResponse.Content.ReadAsAsync(); + + var noteResponse = await GiteaFixture.GiteaClient.Value.GetAsync($"repos/{org}/{targetRepo}/git/notes/{commit.Sha}"); + + var notesNode = JsonNode.Parse(await noteResponse.Content.ReadAsStringAsync()); + notesNode!["message"]!.ToString().Should().Be("studio-commit"); + } + } +} diff --git a/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryControllerGiteaIntegrationTests.cs b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/RepositoryControllerGiteaIntegrationTests.cs similarity index 88% rename from backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryControllerGiteaIntegrationTests.cs rename to backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/RepositoryControllerGiteaIntegrationTests.cs index b5635df8ca0..42dbbddfa31 100644 --- a/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryControllerGiteaIntegrationTests.cs +++ b/backend/tests/Designer.Tests/GiteaIntegrationTests/RepositoryController/RepositoryControllerGiteaIntegrationTests.cs @@ -7,7 +7,6 @@ using System.Net.Mime; using System.Text; using System.Threading.Tasks; -using Altinn.Studio.Designer.Controllers; using Altinn.Studio.Designer.Enums; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.RepositoryClient.Model; @@ -19,7 +18,7 @@ using Polly.Retry; using Xunit; -namespace Designer.Tests.GiteaIntegrationTests +namespace Designer.Tests.GiteaIntegrationTests.RepositoryController { public class RepositoryControllerGiteaIntegrationTests : GiteaIntegrationTestsBase, IClassFixture> { @@ -104,7 +103,7 @@ public async Task Pull_ShouldBeAsExpected(string org) await CreateAppUsingDesigner(org, targetRepo); // Create a file in gitea - using var createFileContent = new StringContent(CreateFileJsonPayload("I am a new file created in gitea", "test commit"), Encoding.UTF8, MediaTypeNames.Application.Json); + using var createFileContent = new StringContent(GenerateCommitJsonPayload("I am a new file created in gitea", "test commit"), Encoding.UTF8, MediaTypeNames.Application.Json); using HttpResponseMessage createFileResponse = await GiteaFixture.GiteaClient.Value.PostAsync($"repos/{org}/{targetRepo}/contents/test2.txt", createFileContent); createFileResponse.StatusCode.Should().Be(HttpStatusCode.Created); @@ -222,7 +221,7 @@ public async Task Commit_AndPush_NonPulled_ShouldReturnConflict(string org) await CreateAppUsingDesigner(org, targetRepo); // Create a file in gitea - using var createFileContent = new StringContent(CreateFileJsonPayload("I am a new file created in gitea", "test commit"), Encoding.UTF8, MediaTypeNames.Application.Json); + using var createFileContent = new StringContent(GenerateCommitJsonPayload("I am a new file created in gitea", "test commit"), Encoding.UTF8, MediaTypeNames.Application.Json); using HttpResponseMessage createFileResponse = await GiteaFixture.GiteaClient.Value.PostAsync($"repos/{org}/{targetRepo}/contents/test2.txt", createFileContent); createFileResponse.StatusCode.Should().Be(HttpStatusCode.Created); @@ -232,33 +231,6 @@ public async Task Commit_AndPush_NonPulled_ShouldReturnConflict(string org) using var commitAndPushContent = new StringContent(GetCommitInfoJson("test commit", org, targetRepo), Encoding.UTF8, MediaTypeNames.Application.Json); using HttpResponseMessage commitAndPushResponse = await HttpClient.PostAsync($"designer/api/repos/repo/{org}/{targetRepo}/commit-and-push", commitAndPushContent); commitAndPushResponse.StatusCode.Should().Be(HttpStatusCode.Conflict); - } - - private static string GetCommitInfoJson(string text, string org, string repository) => - @$"{{ - ""message"": ""{text}"", - ""org"": ""{org}"", - ""repository"": ""{repository}"" - }}"; - - private static string CreateFileJsonPayload(string text, string message) => - @$"{{ - ""author"": {{ - ""email"": ""{GiteaConstants.AdminEmail}"", - ""name"": ""{GiteaConstants.AdminUser}"" - }}, - ""committer"": {{ - ""email"": ""{GiteaConstants.AdminEmail}"", - ""name"": ""{GiteaConstants.AdminUser}"" - }}, - ""content"": ""{Convert.ToBase64String(Encoding.UTF8.GetBytes(text))}"", - ""dates"": {{ - ""author"": ""{DateTime.Now:O}"", - ""committer"": ""{DateTime.Now:O}"" - }}, - ""message"": ""{message}"", - ""signoff"": true - }}"; } }