✨ (dev): Seed richer demo data and skip onboarding on dev login
Changes
5 files changed, +200 -2
MODIFY
README.md
+7 -1
@@ -207,9 +207,15 @@
207
207
208
208
Two `%dev`-only flags are set in `application.properties` (both default `false` in all other profiles):
209
209
210
-- `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.
210
+- `gitshark.dev.seed-data=true` — on startup, `DevDataSeeder` idempotently creates user `alice` owning a public repository `demo` with a Markdown README on `main`, an open merge request (`feature` → `main`, with a sample review comment) and four issues in different states with short and long Markdown descriptions. A fresh dev instance is never empty.
211
211
- `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).
212
212
213
+Dev/test Keycloak imports `src/main/resources/quarkus-realm.json` (users `alice`/`alice` and
214
+`bob`/`bob`, client `quarkus-app` with the `profile` and `email` client scopes). The app requests
215
+`quarkus.oidc.authentication.scopes=profile,email`, so the ID token carries `preferred_username`,
216
+`name` and `email` — logging in as `alice` adopts the seeded account directly and skips the
217
+onboarding page.
218
+
213
219
## Persisted data
214
220
215
221
| Store | What |
MODIFY
src/main/java/de/workaround/dev/DevDataSeeder.java
+86 -1
@@ -8,8 +8,10 @@
8
8
import org.jboss.logging.Logger;
9
9
10
10
import de.workaround.git.GitRepositoryService;
11
+import de.workaround.git.IssueService;
11
12
import de.workaround.git.MergeRequestCommentService;
12
13
import de.workaround.git.MergeRequestService;
14
+import de.workaround.model.Issue;
13
15
import de.workaround.model.MergeRequest;
14
16
import de.workaround.model.Repository;
15
17
import de.workaround.model.User;
@@ -49,6 +51,9 @@
49
51
@Inject
50
52
MergeRequest.Repo mergeRequestRepo;
51
53
54
+ @Inject
55
+ IssueService issueService;
56
+
52
57
@ConfigProperty(name = "gitshark.dev.seed-data", defaultValue = "false")
53
58
boolean enabled;
54
59
@@ -92,6 +97,59 @@
92
97
}
93
98
94
99
seedMergeRequest(alice, demo, bare);
100
+ seedIssues(alice, demo);
101
+ }
102
+
103
+ /**
104
+ * Ensures a handful of demo issues exist so the issue list, status filters and Markdown rendering
105
+ * have something to show locally. Idempotent: seeded only while the repository has no issues at all.
106
+ */
107
+ private void seedIssues(User alice, Repository demo)
108
+ {
109
+ if (!issueService.list(demo).isEmpty())
110
+ {
111
+ return;
112
+ }
113
+ issueService.create(alice, demo, "Add a CONTRIBUTING guide",
114
+ "A short note on how to propose changes would help newcomers.");
115
+ Issue inDevelopment = issueService.create(alice, demo, "Render Markdown in issue descriptions",
116
+ """
117
+ Issue descriptions support **Markdown**, so this issue shows the common elements in one place.
118
+
119
+ ## Motivation
120
+
121
+ A demo instance should let you see at a glance how formatted text renders:
122
+
123
+ - **bold** and *italic* inline styles
124
+ - [links](https://example.com) and `inline code`
125
+ - nested lists
126
+ - like this one
127
+
128
+ ## Proposed approach
129
+
130
+ 1. Parse the description with the same renderer the README uses.
131
+ 2. Sanitize the resulting HTML.
132
+ 3. Cache the rendered fragment alongside the issue.
133
+
134
+ > Rendering and sanitizing must stay in sync — an unsanitized renderer would be an XSS hole.
135
+
136
+ ```java
137
+ String html = markdown.render(issue.description);
138
+ ```
139
+
140
+ This description is intentionally long so list truncation and detail pages are easy to check.""");
141
+ Issue done = issueService.create(alice, demo, "Seed a demo repository on dev startup",
142
+ """
143
+ Done — a freshly started dev instance seeds `alice/demo` automatically.
144
+
145
+ ```bash
146
+ ./mvnw quarkus:dev
147
+ ```""");
148
+ issueService.updateStatus(alice, inDevelopment, Issue.Status.IN_DEVELOPMENT);
149
+ issueService.updateStatus(alice, done, Issue.Status.DONE);
150
+ issueService.create(alice, demo, "Show an empty state on the issues page",
151
+ "When a repository has no issues yet, the list should explain how to create the first one.");
152
+ LOG.infof("Seeded demo issues on %s/%s", alice.username, demo.name);
95
153
}
96
154
97
155
/**
@@ -179,7 +237,34 @@
179
237
try (Git git = Git.cloneRepository().setURI(barePath.toUri().toString()).setDirectory(work.toFile()).call())
180
238
{
181
239
Files.writeString(work.resolve("README.md"),
182
- "# demo\n\nDemo repository seeded for local development.\n");
240
+ """
241
+ # demo
242
+
243
+ Demo repository seeded for local development. It exists so a freshly started
244
+ dev instance has something to browse: a README, issues, and an open merge request.
245
+
246
+ ## What's in here
247
+
248
+ - This `README.md` on `main`
249
+ - An `OVERVIEW.md` waiting on the `feature` branch (see the open merge request)
250
+ - A few issues in different states with Markdown descriptions
251
+
252
+ ## Try it
253
+
254
+ Clone the repository and push a change:
255
+
256
+ ```bash
257
+ git clone http://localhost:8080/alice/demo.git
258
+ cd demo
259
+ git commit --allow-empty -m "Test commit"
260
+ git push
261
+ ```
262
+
263
+ ## Notes
264
+
265
+ Everything in this repository is seeded by `DevDataSeeder` on startup and only
266
+ when missing, so restarts never duplicate data.
267
+ """);
183
268
git.add().addFilepattern(".").call();
184
269
git.commit().setMessage("Initial commit").setSign(false)
185
270
.setAuthor(DEMO_USER, DEMO_USER + "@demo.local")
MODIFY
src/main/resources/application.properties
+3 -0
@@ -23,6 +23,9 @@
23
23
%dev,test.quarkus.oidc.client-id=quarkus-app
24
24
%dev,test.quarkus.oidc.credentials.secret=secret
25
25
quarkus.oidc.application-type=web-app
26
+# Without these scopes the ID token carries only the subject: no preferred_username (dev
27
+# adopt-username can never match, every login lands on onboarding), no name, no email.
28
+quarkus.oidc.authentication.scopes=profile,email
26
29
# No RP-Initiated Logout: kanidm does not advertise an end_session_endpoint, and setting
27
30
# quarkus.oidc.logout.path makes Quarkus validate that endpoint at startup and abort boot when
28
31
# it is missing. Logout is therefore local session only (handled by the app).
ADD
src/main/resources/quarkus-realm.json
+61 -0
@@ -0,0 +1,61 @@
1
+{
2
+ "realm": "quarkus",
3
+ "enabled": true,
4
+ "sslRequired": "none",
5
+ "registrationAllowed": false,
6
+ "users": [
7
+ {
8
+ "username": "alice",
9
+ "enabled": true,
10
+ "firstName": "Alice",
11
+ "lastName": "Demo",
12
+ "email": "alice@demo.local",
13
+ "emailVerified": true,
14
+ "credentials": [
15
+ {
16
+ "type": "password",
17
+ "value": "alice",
18
+ "temporary": false
19
+ }
20
+ ]
21
+ },
22
+ {
23
+ "username": "bob",
24
+ "enabled": true,
25
+ "firstName": "Bob",
26
+ "lastName": "Demo",
27
+ "email": "bob@demo.local",
28
+ "emailVerified": true,
29
+ "credentials": [
30
+ {
31
+ "type": "password",
32
+ "value": "bob",
33
+ "temporary": false
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "clients": [
39
+ {
40
+ "clientId": "quarkus-app",
41
+ "enabled": true,
42
+ "protocol": "openid-connect",
43
+ "publicClient": false,
44
+ "secret": "secret",
45
+ "standardFlowEnabled": true,
46
+ "directAccessGrantsEnabled": true,
47
+ "redirectUris": [
48
+ "*"
49
+ ],
50
+ "webOrigins": [
51
+ "*"
52
+ ],
53
+ "defaultClientScopes": [
54
+ "basic",
55
+ "profile",
56
+ "email"
57
+ ],
58
+ "optionalClientScopes": []
59
+ }
60
+ ]
61
+}
MODIFY
src/test/java/de/workaround/dev/DevDataSeederTest.java
+43 -0
@@ -1,5 +1,6 @@
1
1
package de.workaround.dev;
2
2
3
+import java.nio.charset.StandardCharsets;
3
4
import java.nio.file.Path;
4
5
import java.util.List;
5
6
@@ -7,7 +8,9 @@
7
8
8
9
import de.workaround.git.GitBrowseService;
9
10
import de.workaround.git.GitRepositoryService;
11
+import de.workaround.git.IssueService;
10
12
import de.workaround.git.MergeRequestCommentService;
13
+import de.workaround.model.Issue;
11
14
import de.workaround.model.MergeRequest;
12
15
import de.workaround.model.Repository;
13
16
import io.quarkus.test.junit.QuarkusTest;
@@ -35,6 +38,9 @@
35
38
@Inject
36
39
MergeRequestCommentService comments;
37
40
41
+ @Inject
42
+ IssueService issues;
43
+
38
44
@Test
39
45
void seedsAliceWithDemoRepoContainingACommit()
40
46
{
@@ -85,4 +91,41 @@
85
91
"re-seeding must not add duplicate commits to the feature branch");
86
92
}
87
93
94
+ @Test
95
+ void seedsAReadmeWithRicherMarkdown()
96
+ {
97
+ seeder.seed();
98
+ Repository demo = repositories.find("alice", "demo").orElseThrow();
99
+
100
+ GitBrowseService.BlobView readme = browse.blob(repositories.repositoryPath(demo), "main", "README.md")
101
+ .orElseThrow(() -> new AssertionError("README.md missing on main"));
102
+ String content = new String(readme.content(), StandardCharsets.UTF_8);
103
+ assertTrue(content.contains("## "), "README must contain markdown section headings");
104
+ assertTrue(content.contains("```"), "README must contain a fenced code block");
105
+ assertTrue(content.contains("- "), "README must contain a list");
106
+ }
107
+
108
+ @Test
109
+ void seedsIssuesWithVariedMarkdownDescriptionsAndIsIdempotent()
110
+ {
111
+ seeder.seed();
112
+ Repository demo = repositories.find("alice", "demo").orElseThrow();
113
+
114
+ List<Issue> seeded = issues.list(demo);
115
+ assertEquals(4, seeded.size(), "four demo issues must be seeded");
116
+ assertTrue(seeded.stream().anyMatch(i -> i.description != null && i.description.length() > 400),
117
+ "one issue must carry a long markdown description");
118
+ assertTrue(seeded.stream().anyMatch(i -> i.description != null && i.description.length() < 120),
119
+ "one issue must carry a short description");
120
+ assertTrue(seeded.stream().anyMatch(i -> i.description != null && i.description.contains("```")),
121
+ "one issue description must contain a fenced code block");
122
+ assertTrue(seeded.stream().anyMatch(i -> i.status == Issue.Status.DONE),
123
+ "one issue must already be done so status filters are demoable");
124
+ assertTrue(seeded.stream().anyMatch(i -> i.status == Issue.Status.IN_DEVELOPMENT),
125
+ "one issue must be in development");
126
+
127
+ seeder.seed();
128
+ assertEquals(4, issues.list(demo).size(), "re-seeding must not duplicate issues");
129
+ }
130
+
88
131
}