gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

✨ feat(dev): seed demo data for user alice on dev startup

83e9c5df1491d50f2fcf54d5fb28b05c6c81c2fb · Miggi · 2026-06-26T13:29:26Z

Changes

6 files changed, +302 -7

MODIFY README.md +7 -0
diff --git a/README.md b/README.md
index 45fa376..921d574 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,13 @@
86 86 Native build uses a container build automatically when no local GraalVM is present
87 87 (`-Dquarkus.native.container-build=true` to force). Binary: `target/git-shark-1.0-SNAPSHOT-runner`.
88 88
89 +### Dev mode seed data
90 +
91 +Two `%dev`-only flags are set in `application.properties` (both default `false` in all other profiles):
92 +
93 +- `gitshark.dev.seed-data=true` — on startup, `DevDataSeeder` idempotently creates user `alice` owning a public repository `demo` with one commit (`README.md`, "Initial commit"). A fresh dev instance is never empty.
94 +- `gitshark.dev.adopt-username=true` — Keycloak Dev Services mint a fresh OIDC subject on every run, so the seeded `alice` row would otherwise collide on login. With this flag, an unknown subject whose username matches an existing account re-keys that account to the new subject instead of rejecting the login. **Never enabled in production** (re-keying from a username claim is an account-takeover vector).
95 +
89 96 ## Persisted data
90 97
91 98 | Store | What |
MODIFY src/main/java/de/workaround/account/UserProvisioningService.java +28 -7
diff --git a/src/main/java/de/workaround/account/UserProvisioningService.java b/src/main/java/de/workaround/account/UserProvisioningService.java
index f3b4cd1..04ad000 100644
--- a/src/main/java/de/workaround/account/UserProvisioningService.java
+++ b/src/main/java/de/workaround/account/UserProvisioningService.java
@@ -4,6 +4,7 @@
4 4 import jakarta.enterprise.context.ApplicationScoped;
5 5 import jakarta.inject.Inject;
6 6 import jakarta.transaction.Transactional;
7 +import org.eclipse.microprofile.config.inject.ConfigProperty;
7 8
8 9 /**
9 10 * Creates or updates the local user record from OIDC token claims. The OIDC subject is the
@@ -15,19 +16,39 @@
15 16 @Inject
16 17 User.Repo users;
17 18
19 + // Dev convenience only: Keycloak Dev Services mint a fresh OIDC subject for the seeded
20 + // alice/bob users on every run, so a real login would never match a pre-seeded row by
21 + // subject and would collide on the unique username. When enabled (%dev), an unknown subject
22 + // adopts the existing same-username row instead of inserting a duplicate. Off in prod —
23 + // trusting a username claim to re-key an account would be an account-takeover vector.
24 + @ConfigProperty(name = "gitshark.dev.adopt-username", defaultValue = "false")
25 + boolean adoptExistingUsername;
26 +
18 27 @Transactional
19 28 public User provision(String oidcSub, String username, String displayName, String email)
20 29 {
21 - User user = users.findByOidcSubOptional(oidcSub).orElseGet(() -> {
22 - User created = new User();
23 - created.oidcSub = oidcSub;
24 - created.username = username;
25 - created.persist();
26 - return created;
27 - });
30 + User user = users.findByOidcSubOptional(oidcSub).orElseGet(() -> provisionMissing(oidcSub, username));
28 31 user.displayName = displayName;
29 32 user.email = email;
30 33 return user;
31 34 }
32 35
36 + private User provisionMissing(String oidcSub, String username)
37 + {
38 + if (adoptExistingUsername)
39 + {
40 + User existing = users.findByUsername(username).orElse(null);
41 + if (existing != null)
42 + {
43 + existing.oidcSub = oidcSub;
44 + return existing;
45 + }
46 + }
47 + User created = new User();
48 + created.oidcSub = oidcSub;
49 + created.username = username;
50 + created.persist();
51 + return created;
52 + }
53 +
33 54 }
ADD src/main/java/de/workaround/dev/DevDataSeeder.java +146 -0
diff --git a/src/main/java/de/workaround/dev/DevDataSeeder.java b/src/main/java/de/workaround/dev/DevDataSeeder.java
new file mode 100644
index 0000000..760192c
--- /dev/null
+++ b/src/main/java/de/workaround/dev/DevDataSeeder.java
@@ -0,0 +1,146 @@
1 +package de.workaround.dev;
2 +
3 +import java.nio.file.Files;
4 +import java.nio.file.Path;
5 +
6 +import org.eclipse.jgit.api.Git;
7 +import org.eclipse.jgit.transport.RefSpec;
8 +import org.jboss.logging.Logger;
9 +
10 +import de.workaround.git.GitRepositoryService;
11 +import de.workaround.model.Repository;
12 +import de.workaround.model.User;
13 +import io.quarkus.runtime.StartupEvent;
14 +import jakarta.enterprise.context.ApplicationScoped;
15 +import jakarta.enterprise.event.Observes;
16 +import jakarta.inject.Inject;
17 +import jakarta.transaction.Transactional;
18 +import org.eclipse.microprofile.config.inject.ConfigProperty;
19 +
20 +/**
21 + * Seeds a demo user (alice) owning a public "demo" repository with one commit, so a freshly
22 + * started dev instance is not empty. Enabled only when {@code gitshark.dev.seed-data=true}
23 + * (set under the {@code %dev} profile); idempotent, so restarts never duplicate data.
24 + */
25 +@ApplicationScoped
26 +public class DevDataSeeder
27 +{
28 + private static final Logger LOG = Logger.getLogger(DevDataSeeder.class);
29 +
30 + private static final String DEMO_USER = "alice";
31 + private static final String DEMO_REPO = "demo";
32 +
33 + @Inject
34 + GitRepositoryService repositories;
35 +
36 + @Inject
37 + User.Repo users;
38 +
39 + @ConfigProperty(name = "gitshark.dev.seed-data", defaultValue = "false")
40 + boolean enabled;
41 +
42 + void onStart(@Observes StartupEvent event)
43 + {
44 + if (enabled)
45 + {
46 + seed();
47 + }
48 + }
49 +
50 + /**
51 + * Ensures the demo data exists. Safe to call repeatedly: the user and repository are created
52 + * only when absent, and the seed commit is written only when the repository has no commits.
53 + */
54 + @Transactional
55 + public void seed()
56 + {
57 + User alice = users.findByUsername(DEMO_USER).orElseGet(() -> {
58 + User created = new User();
59 + created.oidcSub = "dev:" + DEMO_USER;
60 + created.username = DEMO_USER;
61 + created.displayName = "Alice (demo)";
62 + created.email = DEMO_USER + "@demo.local";
63 + created.persist();
64 + return created;
65 + });
66 +
67 + Repository demo = repositories.find(alice.username, DEMO_REPO).orElse(null);
68 + if (demo == null)
69 + {
70 + demo = repositories.create(alice, DEMO_REPO, Repository.Visibility.PUBLIC,
71 + "Demo repository seeded for local development");
72 + }
73 +
74 + Path bare = repositories.repositoryPath(demo);
75 + if (hasNoCommits(bare))
76 + {
77 + seedInitialCommit(bare);
78 + LOG.infof("Seeded demo data: %s/%s with an initial commit", alice.username, demo.name);
79 + }
80 + }
81 +
82 + private static boolean hasNoCommits(Path barePath)
83 + {
84 + try (Git git = Git.open(barePath.toFile()))
85 + {
86 + return git.getRepository().resolve("refs/heads/main") == null;
87 + }
88 + catch (Exception e)
89 + {
90 + throw new IllegalStateException("Failed to inspect demo repository at " + barePath, e);
91 + }
92 + }
93 +
94 + private static void seedInitialCommit(Path barePath)
95 + {
96 + Path work = null;
97 + try
98 + {
99 + work = Files.createTempDirectory("git-shark-seed");
100 + try (Git git = Git.cloneRepository().setURI(barePath.toUri().toString()).setDirectory(work.toFile()).call())
101 + {
102 + Files.writeString(work.resolve("README.md"),
103 + "# demo\n\nDemo repository seeded for local development.\n");
104 + git.add().addFilepattern(".").call();
105 + git.commit().setMessage("Initial commit").setSign(false)
106 + .setAuthor(DEMO_USER, DEMO_USER + "@demo.local")
107 + .setCommitter(DEMO_USER, DEMO_USER + "@demo.local").call();
108 + git.push().setRefSpecs(new RefSpec("HEAD:refs/heads/main")).call();
109 + }
110 + }
111 + catch (Exception e)
112 + {
113 + throw new IllegalStateException("Failed to seed initial commit into " + barePath, e);
114 + }
115 + finally
116 + {
117 + deleteRecursively(work);
118 + }
119 + }
120 +
121 + private static void deleteRecursively(Path root)
122 + {
123 + if (root == null)
124 + {
125 + return;
126 + }
127 + try (var paths = Files.walk(root))
128 + {
129 + paths.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
130 + try
131 + {
132 + Files.deleteIfExists(p);
133 + }
134 + catch (Exception ignored)
135 + {
136 + // best-effort cleanup of a temp working clone
137 + }
138 + });
139 + }
140 + catch (Exception ignored)
141 + {
142 + // best-effort cleanup
143 + }
144 + }
145 +
146 +}
MODIFY src/main/resources/application.properties +7 -0
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 7ad8fe4..8b6b7f4 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -23,6 +23,13 @@
23 23 quarkus.oidc.logout.path=/logout
24 24 quarkus.oidc.logout.post-logout-path=/
25 25
26 +# Dev-only convenience (see DevDataSeeder / UserProvisioningService):
27 +# - seed-data: on dev startup, create user alice + a public "demo" repo with one commit (idempotent).
28 +# - adopt-username: let a login adopt the seeded same-username row even though Keycloak Dev Services
29 +# mint a fresh OIDC subject each run. Both default false; never enable in production.
30 +%dev.gitshark.dev.seed-data=true
31 +%dev.gitshark.dev.adopt-username=true
32 +
26 33 # Pages that require a logged-in user (git transport does its own auth)
27 34 quarkus.http.auth.permission.authenticated.paths=/settings/*,/repos/new,/login
28 35 quarkus.http.auth.permission.authenticated.policy=authenticated
ADD src/test/java/de/workaround/account/UserProvisioningAdoptTest.java +59 -0
diff --git a/src/test/java/de/workaround/account/UserProvisioningAdoptTest.java b/src/test/java/de/workaround/account/UserProvisioningAdoptTest.java
new file mode 100644
index 0000000..823bf23
--- /dev/null
+++ b/src/test/java/de/workaround/account/UserProvisioningAdoptTest.java
@@ -0,0 +1,59 @@
1 +package de.workaround.account;
2 +
3 +import java.util.Map;
4 +import java.util.UUID;
5 +
6 +import org.junit.jupiter.api.Test;
7 +
8 +import de.workaround.model.User;
9 +import io.quarkus.test.TestTransaction;
10 +import io.quarkus.test.junit.QuarkusTest;
11 +import io.quarkus.test.junit.QuarkusTestProfile;
12 +import io.quarkus.test.junit.TestProfile;
13 +import jakarta.inject.Inject;
14 +
15 +import static org.junit.jupiter.api.Assertions.assertEquals;
16 +
17 +/**
18 + * Dev-only adoption: when the OIDC subject is unknown but a user with the same username already
19 + * exists (e.g. a seeded dev user), provisioning adopts that row instead of creating a duplicate
20 + * (which the unique username constraint would reject).
21 + */
22 +@QuarkusTest
23 +@TestProfile(UserProvisioningAdoptTest.AdoptProfile.class)
24 +class UserProvisioningAdoptTest
25 +{
26 + public static class AdoptProfile implements QuarkusTestProfile
27 + {
28 + @Override
29 + public Map<String, String> getConfigOverrides()
30 + {
31 + return Map.of("gitshark.dev.adopt-username", "true");
32 + }
33 + }
34 +
35 + @Inject
36 + UserProvisioningService service;
37 +
38 + @Inject
39 + User.Repo users;
40 +
41 + @Test
42 + @TestTransaction
43 + void adoptsExistingUsernameWhenSubjectUnknown()
44 + {
45 + String username = "adopt-" + UUID.randomUUID();
46 + User seeded = new User();
47 + seeded.oidcSub = "seed:" + username;
48 + seeded.username = username;
49 + seeded.persist();
50 +
51 + String loginSub = "kc:" + UUID.randomUUID();
52 + User logged = service.provision(loginSub, username, "Adopted", "adopt@example.com");
53 +
54 + assertEquals(seeded.id, logged.id, "must adopt the existing row, not create a duplicate");
55 + assertEquals(loginSub, users.findById(seeded.id).oidcSub, "adopted row takes the login subject");
56 + assertEquals("Adopted", logged.displayName);
57 + }
58 +
59 +}
ADD src/test/java/de/workaround/dev/DevDataSeederTest.java +55 -0
diff --git a/src/test/java/de/workaround/dev/DevDataSeederTest.java b/src/test/java/de/workaround/dev/DevDataSeederTest.java
new file mode 100644
index 0000000..a1434e7
--- /dev/null
+++ b/src/test/java/de/workaround/dev/DevDataSeederTest.java
@@ -0,0 +1,55 @@
1 +package de.workaround.dev;
2 +
3 +import java.nio.file.Path;
4 +
5 +import org.junit.jupiter.api.Test;
6 +
7 +import de.workaround.git.GitBrowseService;
8 +import de.workaround.git.GitRepositoryService;
9 +import de.workaround.model.Repository;
10 +import io.quarkus.test.junit.QuarkusTest;
11 +import jakarta.inject.Inject;
12 +
13 +import static org.junit.jupiter.api.Assertions.assertEquals;
14 +import static org.junit.jupiter.api.Assertions.assertFalse;
15 +import static org.junit.jupiter.api.Assertions.assertTrue;
16 +
17 +@QuarkusTest
18 +class DevDataSeederTest
19 +{
20 + @Inject
21 + DevDataSeeder seeder;
22 +
23 + @Inject
24 + GitRepositoryService repositories;
25 +
26 + @Inject
27 + GitBrowseService browse;
28 +
29 + @Test
30 + void seedsAliceWithDemoRepoContainingACommit()
31 + {
32 + seeder.seed();
33 +
34 + Repository demo = repositories.find("alice", "demo").orElseThrow(() -> new AssertionError("alice/demo missing"));
35 + assertEquals(Repository.Visibility.PUBLIC, demo.visibility);
36 +
37 + Path bare = repositories.repositoryPath(demo);
38 + assertFalse(browse.isEmpty(bare), "demo repository must have commits");
39 + assertTrue(browse.commitCount(bare, "main") >= 1, "demo repository must contain at least one commit");
40 + }
41 +
42 + @Test
43 + void seedingIsIdempotent()
44 + {
45 + seeder.seed();
46 + Path firstBare = repositories.repositoryPath(repositories.find("alice", "demo").orElseThrow());
47 + int commitsAfterFirst = browse.commitCount(firstBare, "main");
48 +
49 + seeder.seed();
50 + Repository demo = repositories.find("alice", "demo").orElseThrow();
51 + assertEquals(commitsAfterFirst, browse.commitCount(repositories.repositoryPath(demo), "main"),
52 + "re-seeding must not add duplicate commits or repositories");
53 + }
54 +
55 +}

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog