Skip to content

fix(server): provision first-login git provider users - #971

Merged
FelixTJDietrich merged 5 commits into
mainfrom
fix/server-first-login-git-provider-user
Apr 8, 2026
Merged

fix(server): provision first-login git provider users#971
FelixTJDietrich merged 5 commits into
mainfrom
fix/server-first-login-git-provider-user

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Description

Provision the authenticated git-provider user on first access to account settings so GitLab users do not get a 404 before any workspace creation path has materialized their user row.

The bootstrap logic is extracted into a dedicated service and reused by workspace provisioning, avoiding controller-to-workspace-service coupling.

How to Test

  1. Run npm run format && npm run check.
  2. Run npm run build:intelligence-service.
  3. Run npm run test:intelligence-service:unit.
  4. Run cd server/application-server && ./mvnw test -Dsurefire.includedGroups="unit" -Dmaven.test.skip=false -T 2C --batch-mode -q.
  5. Run cd server/application-server && ./mvnw -q -Dtest=AccountControllerIntegrationTest,GitLabWorkspaceCreationIntegrationTest test.

Summary by CodeRabbit

  • New Features

    • Automatic provisioning of Git provider accounts (GitLab/GitHub) when users access account settings; settings endpoint now resolves authentication tokens to identify/provision users.
  • Refactor

    • Git-provider user provisioning moved into a dedicated service for clearer responsibility and reuse.
  • Tests

    • Added integration test verifying automatic provisioning and extended test security mocks for GitLab scenarios.

@FelixTJDietrich
FelixTJDietrich requested a review from a team as a code owner April 7, 2026 22:27
Copilot AI review requested due to automatic review settings April 7, 2026 22:27
@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36567020-9f3a-402c-b2c5-57a44925feb0

📥 Commits

Reviewing files that changed from the base of the PR and between a53be72 and 03aeee6.

📒 Files selected for processing (1)
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java

📝 Walkthrough

Walkthrough

Adds AuthenticatedGitProviderUserService to centralize resolving/provisioning users from JWT claims. AccountController endpoints now accept JwtAuthenticationToken and delegate user resolution; WorkspaceProvisioningService delegates authenticated-user checks to the new service. Tests and TestSecurityConfig updated to cover GitLab provisioning.

Changes

Cohort / File(s) Summary
New User Provisioning Service
server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java
New Spring @Service that extracts Git provider IDs from JWT, finds/creates GitProvider, and upserts/verifies User records. Exposes resolveOrProvisionCurrentUser and ensureCurrentGitLabUserExists (transactional).
Controller & Provisioning Delegation
server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java, server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java
AccountController constructor now requires AuthenticatedGitProviderUserService; getUserSettings/updateUserSettings accept @AuthenticationPrincipal JwtAuthenticationToken and use helper to resolve/provision current user. WorkspaceProvisioningService delegates ensureAuthenticatedUserExists to the new service.
Integration Test & Test Security
server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java, server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/TestSecurityConfig.java
Adds integration test verifying /user/settings provisions a GitLab user when missing. Mock JWT decoder extended to recognize mock-jwt-token-for-gitlab-user with gitlab_id and identity_provider claims.
Unit Test Wiring
server/application-server/src/test/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningServiceTest.java
Test updated to inject a mocked AuthenticatedGitProviderUserService into WorkspaceProvisioningService setup; no assertion changes.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant AC as AccountController
    participant AGP as AuthenticatedGitProviderUserService
    participant GPR as GitProviderRepository
    participant UR as UserRepository

    Client->>AC: GET /user/settings (Bearer JWT)
    AC->>AC: receive JwtAuthenticationToken
    AC->>AGP: resolveOrProvisionCurrentUser(null)
    AGP->>AGP: extract login & provider IDs from JWT
    AGP->>UR: findByLogin(login)
    alt user exists
        UR-->>AGP: return user
    else user missing
        AGP->>GPR: findOrCreate(providerType, serverUrl)
        GPR-->>AGP: return git provider
        AGP->>UR: upsertUser(nativeId, avatar, name, provider)
        UR-->>AGP: return created user
    end
    AGP-->>AC: Optional<User>
    AC-->>Client: HTTP 200 (UserSettingsDTO)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through tokens, claims in sight,
Found gitlab ids and stitched users right,
A helper service tended every name,
Controllers call, tests prove the claim,
New users spring where JWTs take flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: provisioning first-login git provider users, which directly addresses the core objective of auto-provisioning authenticated git-provider users on first access.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/server-first-login-git-provider-user

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java (1)

259-289: ⚠️ Potential issue | 🟡 Minor

Remove duplicate Javadoc block.

There are two consecutive Javadoc blocks for the ensureAuthenticatedUserExists method (lines 259-273 and 275-285). The first block is orphaned and should be removed.

📝 Remove the duplicate Javadoc
     public Long resolveOrCreateGitLabUser(String patToken, String serverUrl, String accountLogin) {
         String resolvedServerUrl = resolveGitLabServerUrl(serverUrl);
         return syncGitLabUserForPAT(patToken, resolvedServerUrl, accountLogin);
     }

-    /**
-     * Ensures the currently authenticated Keycloak user has a corresponding git provider
-     * {`@link` User} entity so they can be assigned as workspace owner.
-     *
-     * <p>Reads identity from JWT claims to determine the user's provider:
-     * <ul>
-     *   <li>{`@code` gitlab_id} → creates a GitLab user (uses the given serverUrl)</li>
-     *   <li>{`@code` github_id} → creates a GitHub user</li>
-     * </ul>
-     *
-     * <p>This is needed because first-time users may not have a {`@code` User} entity yet
-     * (it's normally created during sync). Without this, workspace creation would fail
-     * because there is no owner to assign.
-     *
-     * `@param` gitLabServerUrl the GitLab server URL for GitLab users (resolved to default if blank)
-     */
     /**
      * Ensures the currently authenticated Keycloak user has a corresponding git provider
      * {`@link` User} entity so they can be assigned as workspace owner.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java`
around lines 259 - 289, Remove the duplicate Javadoc preceding the
ensureAuthenticatedUserExists method so there is only one Javadoc block
describing its behavior and params; keep the more accurate block that mentions
GitLab identity and the IllegalStateException, delete the orphaned/first
Javadoc, and ensure the method signature public void
ensureAuthenticatedUserExists(String gitLabServerUrl) and its call to
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl)
remain unchanged.
🧹 Nitpick comments (4)
server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java (2)

21-39: Use Lombok annotations for consistency with codebase conventions.

The service should use @Slf4j for logging and @RequiredArgsConstructor for constructor injection per coding guidelines.

♻️ Proposed refactor using Lombok
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;

+@Slf4j
 `@Service`
+@RequiredArgsConstructor
 public class AuthenticatedGitProviderUserService {

-    private static final Logger log = LoggerFactory.getLogger(AuthenticatedGitProviderUserService.class);
     private static final String GITHUB_SERVER_URL = "https://github.com";

     private final UserRepository userRepository;
     private final GitProviderRepository gitProviderRepository;
     private final GitLabProperties gitLabProperties;
-
-    public AuthenticatedGitProviderUserService(
-        UserRepository userRepository,
-        GitProviderRepository gitProviderRepository,
-        GitLabProperties gitLabProperties
-    ) {
-        this.userRepository = userRepository;
-        this.gitProviderRepository = gitProviderRepository;
-        this.gitLabProperties = gitLabProperties;
-    }

As per coding guidelines: "Use constructor injection via @RequiredArgsConstructor annotation" and "Use @Slf4j logging instead of System.out.println()".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java`
around lines 21 - 39, Replace the manual logger and explicit constructor with
Lombok annotations: remove the static Logger field `log` and annotate the class
`AuthenticatedGitProviderUserService` with `@Slf4j` to provide a logger, and
replace the explicit constructor by adding `@RequiredArgsConstructor` to enable
constructor injection for `userRepository`, `gitProviderRepository`, and
`gitLabProperties`; ensure all final fields remain final and delete the explicit
constructor implementation while keeping other constants like
`GITHUB_SERVER_URL` intact.

48-52: Simplify redundant Optional handling.

After checking isEmpty(), using orElseThrow() is redundant. Use get() directly or restructure the flow.

♻️ Suggested simplification
         Optional<String> currentLogin = SecurityUtils.getCurrentUserLogin();
         if (currentLogin.isEmpty()) {
             return Optional.empty();
         }
-        String login = currentLogin.orElseThrow();
+        String login = currentLogin.get();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java`
around lines 48 - 52, The code in AuthenticatedGitProviderUserService
redundantly checks currentLogin.isEmpty() and then calls
currentLogin.orElseThrow(); replace this pattern by either using
currentLogin.get() after the emptiness check or, preferably, restructure to
return/operate on SecurityUtils.getCurrentUserLogin().map(...) so you avoid the
separate isEmpty() check and the orElseThrow() call; locate
SecurityUtils.getCurrentUserLogin() and the local variable currentLogin in the
method and apply one of these simplifications.
server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java (1)

214-221: Consider passing auth context directly to the service.

The helper validates auth via resolveAuthentication() but then calls the service which reads from SecurityContextHolder independently. While functionally correct (Spring populates both consistently), passing the JWT directly would make the data flow more explicit and testable.

This is a low-priority suggestion for future improvement—current implementation works correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java`
around lines 214 - 221, The method resolveOrProvisionCurrentUser in
AccountController currently validates the JwtAuthenticationToken via
resolveAuthentication(auth) but then calls
authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(null) which
makes the service read from SecurityContextHolder; change the call to pass the
JwtAuthenticationToken (e.g.,
authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(auth)) and
update the service method signature (and any internal usages) to accept a
JwtAuthenticationToken so the authentication context is explicit and testable;
ensure any references to resolveOrProvisionCurrentUser in the service and its
tests are adjusted accordingly.
server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java (1)

42-46: Extract provisioned user to avoid repeated orElseThrow() calls.

The same Optional is unwrapped three times. Extract once for clarity and performance.

♻️ Suggested simplification
         var provisionedUser = userRepository.findByLogin("gitlabuser");
         assertThat(provisionedUser).isPresent();
-        assertThat(provisionedUser.orElseThrow().getNativeId()).isEqualTo(18024L);
-        assertThat(provisionedUser.orElseThrow().getProvider().getType()).isEqualTo(GitProviderType.GITLAB);
-        assertThat(provisionedUser.orElseThrow().getProvider().getServerUrl()).isEqualTo("https://gitlab.com");
+        var user = provisionedUser.orElseThrow();
+        assertThat(user.getNativeId()).isEqualTo(18024L);
+        assertThat(user.getProvider().getType()).isEqualTo(GitProviderType.GITLAB);
+        assertThat(user.getProvider().getServerUrl()).isEqualTo("https://gitlab.com");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`
around lines 42 - 46, Extract the Optional once and reuse the unwrapped user to
avoid repeated orElseThrow() calls: in AccountControllerIntegrationTest where
you declare var provisionedUser = userRepository.findByLogin("gitlabuser"),
immediately do var user = provisionedUser.orElseThrow() and then assert on
user.getNativeId(), user.getProvider().getType(), and
user.getProvider().getServerUrl() instead of calling orElseThrow() for each
assertion; this improves clarity and performance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java`:
- Around line 286-289: Update the Javadoc for
WorkspaceProvisioningService.ensureAuthenticatedUserExists to reflect the actual
exception type and scenario: replace or augment the existing `@throws`
IllegalStateException with `@throws` ResponseStatusException (HTTP 409) to
indicate the method delegates to
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists and will throw
a ResponseStatusException when the GitLab identity is missing or conflicts;
mention the HTTP 409 status and the missing GitLab identity condition so the
documentation matches the delegated method's behavior.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`:
- Around line 24-27: Add a per-test cleanup by adding a `@BeforeEach` method in
AccountControllerIntegrationTest that calls databaseTestUtils.cleanDatabase() so
the test getUserSettingsProvisionsGitLabUserWhenMissing() starts from a clean
DB; locate the test class and create a method (annotated with `@BeforeEach`) that
invokes databaseTestUtils.cleanDatabase() before each test run.
- Around line 37-40: The test in AccountControllerIntegrationTest expecting
participateInResearch:false is incorrect because UserPreferences defaults
participateInResearch to true and AccountService.getUserSettings() provisions
defaults via getOrCreatePreferences() and toDTO(); update the assertion in
AccountControllerIntegrationTest to expect true (change .isEqualTo(false) to
.isEqualTo(true)) so the test matches the actual default behavior of
UserPreferences and the getUserSettings() flow.

---

Outside diff comments:
In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java`:
- Around line 259-289: Remove the duplicate Javadoc preceding the
ensureAuthenticatedUserExists method so there is only one Javadoc block
describing its behavior and params; keep the more accurate block that mentions
GitLab identity and the IllegalStateException, delete the orphaned/first
Javadoc, and ensure the method signature public void
ensureAuthenticatedUserExists(String gitLabServerUrl) and its call to
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl)
remain unchanged.

---

Nitpick comments:
In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java`:
- Around line 214-221: The method resolveOrProvisionCurrentUser in
AccountController currently validates the JwtAuthenticationToken via
resolveAuthentication(auth) but then calls
authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(null) which
makes the service read from SecurityContextHolder; change the call to pass the
JwtAuthenticationToken (e.g.,
authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(auth)) and
update the service method signature (and any internal usages) to accept a
JwtAuthenticationToken so the authentication context is explicit and testable;
ensure any references to resolveOrProvisionCurrentUser in the service and its
tests are adjusted accordingly.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java`:
- Around line 21-39: Replace the manual logger and explicit constructor with
Lombok annotations: remove the static Logger field `log` and annotate the class
`AuthenticatedGitProviderUserService` with `@Slf4j` to provide a logger, and
replace the explicit constructor by adding `@RequiredArgsConstructor` to enable
constructor injection for `userRepository`, `gitProviderRepository`, and
`gitLabProperties`; ensure all final fields remain final and delete the explicit
constructor implementation while keeping other constants like
`GITHUB_SERVER_URL` intact.
- Around line 48-52: The code in AuthenticatedGitProviderUserService redundantly
checks currentLogin.isEmpty() and then calls currentLogin.orElseThrow(); replace
this pattern by either using currentLogin.get() after the emptiness check or,
preferably, restructure to return/operate on
SecurityUtils.getCurrentUserLogin().map(...) so you avoid the separate isEmpty()
check and the orElseThrow() call; locate SecurityUtils.getCurrentUserLogin() and
the local variable currentLogin in the method and apply one of these
simplifications.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`:
- Around line 42-46: Extract the Optional once and reuse the unwrapped user to
avoid repeated orElseThrow() calls: in AccountControllerIntegrationTest where
you declare var provisionedUser = userRepository.findByLogin("gitlabuser"),
immediately do var user = provisionedUser.orElseThrow() and then assert on
user.getNativeId(), user.getProvider().getType(), and
user.getProvider().getServerUrl() instead of calling orElseThrow() for each
assertion; this improves clarity and performance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2de6ca40-30f9-4f9f-b712-522fe1c198c9

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc5fee and a1038b2.

📒 Files selected for processing (5)
  • server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java
  • server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java
  • server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/TestSecurityConfig.java

Comment on lines 286 to 289
@Transactional
public void ensureAuthenticatedUserExists(String gitLabServerUrl) {
String login = SecurityUtils.getCurrentUserLoginOrThrow();

// Fast path: if user already exists in ANY provider, we're done
if (userRepository.findByLogin(login).isPresent()) {
return;
}

// Read identity from JWT
Authentication auth =
org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof org.springframework.security.oauth2.jwt.Jwt jwt)) {
throw new IllegalStateException("No JWT found for authenticated user");
}

Long gitlabId = jwt.getClaim("gitlab_id");
if (gitlabId != null) {
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
upsertGitLabUser(gitlabId, login, login, "", resolvedUrl + "/" + login, resolvedUrl, User.Type.USER);
return;
}

// No GitLab identity — check if they at least have a GitHub identity
Long githubId = jwt.getClaim("github_id");
if (githubId != null) {
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.CONFLICT,
"You need to link your GitLab account before creating a GitLab workspace. " +
"Go to Settings → Linked Accounts to connect your GitLab identity."
);
}

throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.CONFLICT,
"No GitLab identity found. Please link your GitLab account in Settings → Linked Accounts."
);
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update Javadoc to reflect new exception types.

The remaining Javadoc at line 284 states @throws IllegalStateException but the delegated method ensureCurrentGitLabUserExists throws ResponseStatusException (HTTP 409) for missing GitLab identity scenarios. Update the documentation to reflect the actual exception behavior.

📝 Fix throws documentation
      * `@param` gitLabServerUrl the GitLab server URL (resolved to default if blank)
-     * `@throws` IllegalStateException if the user has no GitLab identity linked
+     * `@throws` ResponseStatusException with HTTP 409 CONFLICT if the user has no GitLab identity linked
+     * `@throws` IllegalStateException if no JWT is found for the authenticated user
      */
     `@Transactional`
     public void ensureAuthenticatedUserExists(String gitLabServerUrl) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java`
around lines 286 - 289, Update the Javadoc for
WorkspaceProvisioningService.ensureAuthenticatedUserExists to reflect the actual
exception type and scenario: replace or augment the existing `@throws`
IllegalStateException with `@throws` ResponseStatusException (HTTP 409) to
indicate the method delegates to
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists and will throw
a ResponseStatusException when the GitLab identity is missing or conflicts;
mention the HTTP 409 status and the missing GitLab identity condition so the
documentation matches the delegated method's behavior.

Comment on lines +24 to +27
@Test
@DisplayName("GET /user/settings provisions a GitLab user from JWT claims when no user row exists yet")
void getUserSettingsProvisionsGitLabUserWhenMissing() {
assertThat(userRepository.findByLogin("gitlabuser")).isEmpty();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if BaseIntegrationTest has `@BeforeEach` cleanup
rg -n "@BeforeEach|@AfterEach|databaseTestUtils" --type java server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/BaseIntegrationTest.java

# Check how other integration tests handle cleanup
rg -n "@BeforeEach" --type java -A 3 server/application-server/src/test/java/de/tum/in/www1/hephaestus/ | head -50

Repository: ls1intum/Hephaestus

Length of output: 6399


🏁 Script executed:

# Get full BaseIntegrationTest to check for `@BeforeEach` cleanup
cat -n server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/BaseIntegrationTest.java

Repository: ls1intum/Hephaestus

Length of output: 2843


🏁 Script executed:

# Check the AccountControllerIntegrationTest file to see if it has `@BeforeEach`
cat -n server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java

Repository: ls1intum/Hephaestus

Length of output: 2362


🏁 Script executed:

# Check DatabaseTestUtils to understand cleanup requirements
cat -n server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/DatabaseTestUtils.java | head -100

Repository: ls1intum/Hephaestus

Length of output: 4626


Add @BeforeEach cleanup to prevent test flakiness.

The test getUserSettingsProvisionsGitLabUserWhenMissing() asserts an empty precondition (findByLogin("gitlabuser").isEmpty()) but lacks @BeforeEach to invoke databaseTestUtils.cleanDatabase(). While BaseIntegrationTest provides the utility, cleanup must be explicitly called per test—following the pattern used in other integration tests like WorkspaceRepositoryCoverageIntegrationTest. Without it, the test will fail when previous test runs leave residual "gitlabuser" data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`
around lines 24 - 27, Add a per-test cleanup by adding a `@BeforeEach` method in
AccountControllerIntegrationTest that calls databaseTestUtils.cleanDatabase() so
the test getUserSettingsProvisionsGitLabUserWhenMissing() starts from a clean
DB; locate the test class and create a method (annotated with `@BeforeEach`) that
invokes databaseTestUtils.cleanDatabase() before each test run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Provision git-provider users (especially GitLab) on first access to /user/settings so first-time logins don’t hit a 404 before any workspace-creation flow has created a User row.

Changes:

  • Introduce AuthenticatedGitProviderUserService to resolve/provision the current Git provider user from JWT claims.
  • Reuse the new service from both AccountController (settings endpoints) and WorkspaceProvisioningService (GitLab workspace creation gating).
  • Add an integration test and extend the test JWT decoder with GitLab claims.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/user/AuthenticatedGitProviderUserService.java New service that provisions GitLab/GitHub User rows from JWT claims.
server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java Settings endpoints now resolve/provision a git-provider user instead of returning 404 when missing.
server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java Delegates GitLab “ensure user exists/linked” behavior to the new service.
server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java New integration test asserting /user/settings provisions a GitLab user.
server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/TestSecurityConfig.java Adds a mock GitLab JWT token with gitlab_id claim for tests.
Comments suppressed due to low confidence (1)

server/application-server/src/test/java/de/tum/in/www1/hephaestus/testconfig/TestSecurityConfig.java:86

  • TestSecurityConfig’s class-level comment says the mock JWT contains the same realm_access structure as a real Keycloak token, but for tokens with an empty roles array (including the newly added GitLab token) the code omits the realm_access claim entirely. Either always include realm_access (with an empty roles list) or adjust the documentation so tests don’t accidentally rely on a non-production structure.
            // Create a mock JWT that matches the structure expected by the main SecurityConfig
            Map<String, Object> claims = new HashMap<>();
            claims.put("sub", userId);
            claims.put("preferred_username", username);
            claims.put("iss", "https://test-issuer");
            claims.put("aud", "test-audience");

            if ("mock-jwt-token-for-gitlab-user".equals(token)) {
                claims.put("gitlab_id", 18024L);
                claims.put("identity_provider", "gitlab-lrz");
            }

            // Add realm_access with roles (same structure as Keycloak)
            if (roles.length > 0) {
                Map<String, Object> realmAccess = new HashMap<>();
                realmAccess.put("roles", Arrays.asList(roles));
                claims.put("realm_access", realmAccess);
            }

@@ -65,7 +66,8 @@
WorkspaceMembershipRepository workspaceMembershipRepository,
WorkspaceMembershipService workspaceMembershipService,
WorkspaceScopeFilter workspaceScopeFilter,

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

The constructor signature was changed to include AuthenticatedGitProviderUserService; there are direct instantiations of WorkspaceProvisioningService in tests (e.g., WorkspaceProvisioningServiceTest) that will no longer compile until updated to pass this dependency (likely as a mock).

Suggested change
WorkspaceScopeFilter workspaceScopeFilter,
WorkspaceScopeFilter workspaceScopeFilter,
GitLabProperties gitLabProperties
) {
this(
workspaceProperties,
workspaceRepository,
repositoryToMonitorRepository,
workspaceService,
workspaceInstallationService,
workspaceRepositoryMonitorService,
gitHubAppTokenService,
userRepository,
gitProviderRepository,
workspaceMembershipRepository,
workspaceMembershipService,
workspaceScopeFilter,
gitLabProperties,
null
);
}
public WorkspaceProvisioningService(
WorkspaceProperties workspaceProperties,
WorkspaceRepository workspaceRepository,
RepositoryToMonitorRepository repositoryToMonitorRepository,
WorkspaceService workspaceService,
WorkspaceInstallationService workspaceInstallationService,
WorkspaceRepositoryMonitorService workspaceRepositoryMonitorService,
GitHubAppTokenService gitHubAppTokenService,
UserRepository userRepository,
GitProviderRepository gitProviderRepository,
WorkspaceMembershipRepository workspaceMembershipRepository,
WorkspaceMembershipService workspaceMembershipService,
WorkspaceScopeFilter workspaceScopeFilter,

Copilot uses AI. Check for mistakes.
Comment on lines 259 to 286
/**
* Ensures the currently authenticated Keycloak user has a corresponding git provider
* {@link User} entity so they can be assigned as workspace owner.
*
* <p>Reads identity from JWT claims to determine the user's provider:
* <ul>
* <li>{@code gitlab_id} → creates a GitLab user (uses the given serverUrl)</li>
* <li>{@code github_id} → creates a GitHub user</li>
* </ul>
*
* <p>This is needed because first-time users may not have a {@code User} entity yet
* (it's normally created during sync). Without this, workspace creation would fail
* because there is no owner to assign.
*
* @param gitLabServerUrl the GitLab server URL for GitLab users (resolved to default if blank)
*/
/**
* Ensures the currently authenticated Keycloak user has a corresponding git provider
* {@link User} entity so they can be assigned as workspace owner.
*
* <p>For GitLab workspaces, the user must have a linked GitLab identity ({@code gitlab_id}
* in their JWT). If they logged in via GitHub without linking GitLab, this method throws
* so the frontend can prompt them to link their account first.
*
* @param gitLabServerUrl the GitLab server URL (resolved to default if blank)
* @throws IllegalStateException if the user has no GitLab identity linked
*/
@Transactional

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

There are two consecutive Javadoc blocks for ensureAuthenticatedUserExists; the first one (describing provisioning both GitLab and GitHub users) appears to be leftover and now contradicts the method behavior (it delegates to ensureCurrentGitLabUserExists). Please remove/merge the duplicate Javadoc so the method has a single, accurate description.

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +88
if (userRepository.findByLogin(login).isPresent()) {
return;
}

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

ensureCurrentGitLabUserExists returns early if any User with the current login exists, even if that user belongs to a non-GitLab provider. This can bypass the intended 409 conflict when a GitHub-authenticated user (no gitlab_id) already has a GitHub user row and then tries to create a GitLab PAT workspace. Consider only short-circuiting when a GitLab-scoped user exists (matching the resolved GitLab provider), or always validating presence of gitlab_id regardless of existing users.

Suggested change
if (userRepository.findByLogin(login).isPresent()) {
return;
}

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +28
@AutoConfigureWebTestClient
@DisplayName("Account controller integration")
class AccountControllerIntegrationTest extends BaseIntegrationTest {

@Autowired
private WebTestClient webTestClient;

@Autowired
private UserRepository userRepository;

@Test
@DisplayName("GET /user/settings provisions a GitLab user from JWT claims when no user row exists yet")
void getUserSettingsProvisionsGitLabUserWhenMissing() {
assertThat(userRepository.findByLogin("gitlabuser")).isEmpty();

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

This integration test assumes the database does not already contain a user with login "gitlabuser". Other integration tests in this codebase call databaseTestUtils.cleanDatabase() in @BeforeEach to remain independent under parallel execution; this test should do the same (or use a unique per-test login) to avoid flakes.

Copilot generated this review using guidance from repository custom instructions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningServiceTest.java`:
- Around line 62-64: Add unit tests in WorkspaceProvisioningServiceTest that
exercise the new delegation path: mock AuthenticatedGitProviderUserService and
verify WorkspaceProvisioningService.ensureAuthenticatedUserExists(String) calls
the injected service; add one test where the mocked
AuthenticatedGitProviderUserService returns normally and asserts no exception is
thrown and interactions occurred, and add tests where the mock throws a
ResponseStatusException and an IllegalStateException respectively, asserting the
same exceptions propagate from ensureAuthenticatedUserExists. Use the existing
`@Mock` authenticatedGitProviderUserService and the service under test
(WorkspaceProvisioningService) to set up behavior and verify interactions for
these edge cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 19a33c88-45d8-4350-bea8-0a34fc070331

📥 Commits

Reviewing files that changed from the base of the PR and between a1038b2 and 48f6689.

📒 Files selected for processing (1)
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningServiceTest.java

Comment on lines +62 to +64
@Mock
private AuthenticatedGitProviderUserService authenticatedGitProviderUserService;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add unit coverage for the new authenticated-user delegation path.

AuthenticatedGitProviderUserService is now injected, but this test class does not exercise WorkspaceProvisioningService.ensureAuthenticatedUserExists(String) at all. That leaves delegation and exception propagation (ResponseStatusException / IllegalStateException) unverified, even though this is central to the PR behavior.

✅ Proposed test additions
@@
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
@@
 class WorkspaceProvisioningServiceTest {
@@
     `@Test`
+    void shouldDelegateToAuthenticatedGitProviderUserServiceWhenEnsuringAuthenticatedUserExists() {
+        // Arrange
+        String gitLabServerUrl = "https://gitlab.com";
+
+        // Act
+        provisioningService.ensureAuthenticatedUserExists(gitLabServerUrl);
+
+        // Assert
+        verify(authenticatedGitProviderUserService).ensureCurrentGitLabUserExists(gitLabServerUrl);
+    }
+
+    `@Test`
+    void shouldPropagateConflictWhenAuthenticatedGitProviderUserServiceThrowsConflict() {
+        // Arrange
+        String gitLabServerUrl = "https://gitlab.com";
+        ResponseStatusException conflict = new ResponseStatusException(HttpStatus.CONFLICT, "link account");
+        when(authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl)).thenThrow(conflict);
+
+        // Act + Assert
+        assertThatThrownBy(() -> provisioningService.ensureAuthenticatedUserExists(gitLabServerUrl))
+            .isSameAs(conflict);
+    }
+
+    `@Test`
+    void shouldPropagateIllegalStateWhenAuthenticatedGitProviderUserServiceThrowsIllegalState() {
+        // Arrange
+        String gitLabServerUrl = "https://gitlab.com";
+        IllegalStateException illegalState = new IllegalStateException("No JWT found");
+        when(authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl)).thenThrow(illegalState);
+
+        // Act + Assert
+        assertThatThrownBy(() -> provisioningService.ensureAuthenticatedUserExists(gitLabServerUrl))
+            .isSameAs(illegalState);
+    }
+
+    `@Test`
     void bootstrapDefaultPatWorkspace_addsAdminAsMember() throws Exception {

As per coding guidelines: "Focus on risk: cover critical flows and edge cases first when writing tests."

Also applies to: 99-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningServiceTest.java`
around lines 62 - 64, Add unit tests in WorkspaceProvisioningServiceTest that
exercise the new delegation path: mock AuthenticatedGitProviderUserService and
verify WorkspaceProvisioningService.ensureAuthenticatedUserExists(String) calls
the injected service; add one test where the mocked
AuthenticatedGitProviderUserService returns normally and asserts no exception is
thrown and interactions occurred, and add tests where the mock throws a
ResponseStatusException and an IllegalStateException respectively, asserting the
same exceptions propagate from ensureAuthenticatedUserExists. Use the existing
`@Mock` authenticatedGitProviderUserService and the service under test
(WorkspaceProvisioningService) to set up behavior and verify interactions for
these edge cases.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java (1)

25-29: ⚠️ Potential issue | 🔴 Critical

Add @BeforeEach cleanup to ensure test isolation.

The precondition assertion at line 29 (assertThat(userRepository.findByLogin("gitlabuser")).isEmpty()) assumes a clean database state. Per coding guidelines, tests "may run in parallel" and should "assume that there might be data from previous tests." Without explicit cleanup via databaseTestUtils.cleanDatabase() in a @BeforeEach method, this test will fail if another test creates a "gitlabuser" row before this test runs.

🛠️ Proposed fix
 class AccountControllerIntegrationTest extends BaseIntegrationTest {
 
+    `@Autowired`
+    private DatabaseTestUtils databaseTestUtils;
+
     `@Autowired`
     private WebTestClient webTestClient;
 
     `@Autowired`
     private UserRepository userRepository;
 
+    `@BeforeEach`
+    void setUp() {
+        databaseTestUtils.cleanDatabase();
+    }
+
     `@Test`
     `@Transactional`

Also add the imports:

import de.tum.in.www1.hephaestus.testconfig.DatabaseTestUtils;
import org.junit.jupiter.api.BeforeEach;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`
around lines 25 - 29, Add a `@BeforeEach` method in
AccountControllerIntegrationTest that calls DatabaseTestUtils.cleanDatabase() to
ensure a clean DB before each test; import
de.tum.in.www1.hephaestus.testconfig.DatabaseTestUtils and
org.junit.jupiter.api.BeforeEach, inject or use the existing DatabaseTestUtils
instance in the test class, and run databaseTestUtils.cleanDatabase() so the
precondition asserting userRepository.findByLogin("gitlabuser") isEmpty() will
be reliable.
🧹 Nitpick comments (1)
server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java (1)

44-48: Extract user from Optional once to avoid repeated orElseThrow() calls.

Lines 46-48 call orElseThrow() three times on the same Optional. Extract the user once for cleaner assertions.

♻️ Proposed refactor
         var provisionedUser = userRepository.findByLogin("gitlabuser");
         assertThat(provisionedUser).isPresent();
-        assertThat(provisionedUser.orElseThrow().getNativeId()).isEqualTo(18024L);
-        assertThat(provisionedUser.orElseThrow().getProvider().getType()).isEqualTo(GitProviderType.GITLAB);
-        assertThat(provisionedUser.orElseThrow().getProvider().getServerUrl()).isEqualTo("https://gitlab.lrz.de");
+        var user = provisionedUser.orElseThrow();
+        assertThat(user.getNativeId()).isEqualTo(18024L);
+        assertThat(user.getProvider().getType()).isEqualTo(GitProviderType.GITLAB);
+        assertThat(user.getProvider().getServerUrl()).isEqualTo("https://gitlab.lrz.de");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`
around lines 44 - 48, In AccountControllerIntegrationTest, avoid calling
orElseThrow() multiple times on the same Optional returned by
userRepository.findByLogin("gitlabuser"); instead extract the value once (e.g.
assign provisionedUser.orElseThrow() to a local variable) and use that variable
for the subsequent assertions on getNativeId(), getProvider().getType(), and
getProvider().getServerUrl() to make the test clearer and avoid repeated
unwrapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`:
- Around line 25-29: Add a `@BeforeEach` method in
AccountControllerIntegrationTest that calls DatabaseTestUtils.cleanDatabase() to
ensure a clean DB before each test; import
de.tum.in.www1.hephaestus.testconfig.DatabaseTestUtils and
org.junit.jupiter.api.BeforeEach, inject or use the existing DatabaseTestUtils
instance in the test class, and run databaseTestUtils.cleanDatabase() so the
precondition asserting userRepository.findByLogin("gitlabuser") isEmpty() will
be reliable.

---

Nitpick comments:
In
`@server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java`:
- Around line 44-48: In AccountControllerIntegrationTest, avoid calling
orElseThrow() multiple times on the same Optional returned by
userRepository.findByLogin("gitlabuser"); instead extract the value once (e.g.
assign provisionedUser.orElseThrow() to a local variable) and use that variable
for the subsequent assertions on getNativeId(), getProvider().getType(), and
getProvider().getServerUrl() to make the test clearer and avoid repeated
unwrapping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ea1c2dd-8944-4bce-8b75-a717a05dd8ce

📥 Commits

Reviewing files that changed from the base of the PR and between 48f6689 and a53be72.

📒 Files selected for processing (1)
  • server/application-server/src/test/java/de/tum/in/www1/hephaestus/account/AccountControllerIntegrationTest.java

@FelixTJDietrich
FelixTJDietrich merged commit 211f9a0 into main Apr 8, 2026
41 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the fix/server-first-login-git-provider-user branch April 8, 2026 07:13
@github-actions

github-actions Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Preview

Preview has been removed (PR closed)

@FelixTJDietrich

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 0.56.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants