๐ (repos): Move mirrors, danger zone & collaborators behind the Settings tab
Changes
12 files changed, +131 -94
MODIFY
README.md
+6 -5
@@ -19,7 +19,7 @@
19
19
- push and private read authenticate with **personal access tokens** (HTTP Basic password)
20
20
- Clone/fetch/push over `ssh://git@<host>:2222/<owner>/<repo>.git`
21
21
- public-key authentication only; keys managed per user in the UI
22
-- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout โ a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests, plus Collaborators for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) โ every page works fully without JavaScript
22
+- Web UI: an auth-aware header nav (a "Log in" button for visitors; for signed-in users a top-level Following link plus an Account dropdown holding Profile, SSH keys, Access tokens, and Logout โ a JS-free `<details>` menu), landing page with login CTA for visitors (`/`), repository list for authenticated users (`/`), public repository browse at `/explore`, file/tree browser with self-hosted syntax highlighting (extension-based language detection, falls back to plain text for unknown extensions and binary files; Markdown files render to HTML by default with a Rendered/Code toggle), a rendered README (commonmark-java with GFM tables, XSS-safe) shown below the file list on the repository overview page, commit log (paginated), branches, tags (own dedicated page, separate from branches), one-time handle selection (`/onboarding`), profile settings (`/settings/profile`). Every repository sub-page shows a persistent left sidebar with repo identity, a Clone button opening the clone dialog, a pin toggle, and section navigation (Code, Commits, Branches, Tags, Issues, Merge requests, plus Settings for the owner) with per-section counts and active-section highlighting, and the clone panel has copy-to-clipboard buttons for the HTTP and SSH `git clone` commands. Keyboard shortcuts are an optional, progressive enhancement (`?` opens a help overlay, `Escape` closes it, `g h` goes home) โ every page works fully without JavaScript
23
23
- Per-repository issues: title, optional description (rendered as Markdown, XSS-safe), per-repo sequential number (`#1`, `#2`, โฆ), and author; created and managed by the repo owner and collaborators, readable by anyone who can read the repo, via a dedicated "New issue" page; title and description can be edited afterwards via an "Edit issue" page. Issue pages are addressed by number (`โฆ/issues/1`); old UUID URLs redirect permanently to the number form
24
24
- Issues move through a fixed lifecycle (Planned โ In development โ Done); the repo navigation shows the open (Planned + In development) issue count, and Done issues collapse into an "Archive" section on the issues page
25
25
- Issues auto-close from pushed commit messages, GitHub-style (`close(s|d)`/`fix(es|ed)`/`resolve(s|d)` + `#<number>`, e.g. `fixes #12`), over both HTTP and SSH pushes
@@ -31,8 +31,8 @@
31
31
- OIDC login (authorization code flow) via `GET /login`; on first login the user account is created without a username and the browser is redirected to `/onboarding`, where the user picks a URL-safe handle (`^[a-z0-9][a-z0-9-]{0,38}$`, unique across users and organisations). The chosen handle โ not the OIDC `preferred_username` claim (which is an SPN form in kanidm and not URL-safe) โ is used in all repo, SSH, ActivityPub, and webfinger URLs. The `name` claim becomes an editable display name; both can be changed later at `/settings/profile`. A request filter blocks all app pages until a handle is chosen. Logout is local-session only via `POST /logout` (the kanidm provider advertises no `end_session_endpoint`, so RP-Initiated Logout is disabled)
32
32
- Profile pictures: users can upload a PNG/JPEG/GIF/WebP avatar (โค 2 MB, content-type and magic bytes both validated) at `/settings/profile`, stored on the filesystem keyed by user UUID and served publicly at `GET /users/{username}/avatar`; shown wherever a local user is rendered (header nav, repo lists, repo sidebar, issue/MR/comment authors) via a reusable Qute avatar tag, removable, and falling back to an initials badge when absent. Git commit authors and remote federation actors are not local users and keep their existing pseudo-avatars
33
33
- **Collaborators** โ the repository owner can grant other local users read+write access
34
- (one flat role) on a per-repository settings page (`โฆ/settings/collaborators`, linked in the
35
- repo sidebar for the owner). Collaborators can read and push โ UI, HTTP, and SSH alike โ even
34
+ (one flat role) on a per-repository settings page (`โฆ/settings/collaborators`, reached from a
35
+ "Manage collaborators" link on the owner-only repository Settings page). Collaborators can read and push โ UI, HTTP, and SSH alike โ even
36
36
on private repositories, and manage issues and merge requests; deleting the repository,
37
37
managing mirrors, and managing collaborators stay owner-only. Guides:
38
38
[for users](docs/users/collaborators.md), [for admins](docs/admins/collaborators.md)
@@ -49,8 +49,9 @@
49
49
every push (`git push --mirror` semantics, all refs including deletions), over HTTPS with
50
50
stored credentials or over SSH with a server-generated Ed25519 deploy key. Syncs run
51
51
asynchronously (the incoming push never waits or fails because of a mirror), coalesce under
52
- rapid pushes, retry with exponential backoff, and dead-letter with the error visible in the
53
- UI; secrets are encrypted at rest (`GITSHARK_SECRET_KEY`). Guides:
52
+ rapid pushes, retry with exponential backoff, and dead-letter with the error visible on the
53
+ owner-only repository **Settings** page where mirrors are managed; secrets are encrypted at
54
+ rest (`GITSHARK_SECRET_KEY`). Guides:
54
55
[for users](docs/users/mirrors.md), [for admins](docs/admins/mirrors.md),
55
56
[architecture](docs/maintainers/push-mirrors.md)
56
57
- **Federation (ForgeFed / ActivityPub)** โ *opt-in, off by default.* Public repositories are
MODIFY
docs/maintainers/push-mirrors.md
+1 -1
@@ -13,7 +13,7 @@
13
13
| `MirrorKeys` | `mirror/` | Ed25519 deploy-key generation (BouncyCastle), OpenSSH public-key rendering, PKCS#8 PEM round-trip (public key derives from the private key) |
14
14
| `MirrorUrlValidator` | `mirror/` | SSRF guard for targets (scheme, own-host loop check, non-public address rejection) |
15
15
| `web/MirrorResource` | `web/` | Owner-only form endpoints under `/repos/{owner}/{name}/mirrors`; non-owners get 404 |
16
-| Overview template | `RepositoryResource/overview.html` | Mirrors panel (owner-only): list + status, deploy-key display, add/push-now/delete forms |
16
+| Settings template | `RepositoryResource/settings.html` | Mirrors panel (owner-only, under the Settings tab): list + status, deploy-key display, add/push-now/delete forms; `MirrorResource` redirects back to `โฆ/settings` |
17
17
18
18
## Trigger flow
19
19
MODIFY
docs/users/collaborators.md
+2 -2
@@ -23,8 +23,8 @@
23
23
24
24
## Adding a collaborator
25
25
26
-1. Open your repository. As the owner you'll find a **Collaborators** entry in the left
27
- sidebar (it links to `โฆ/settings/collaborators`).
26
+1. Open your repository's **Settings** page (the โ Settings tab in the left sidebar,
27
+ owner only) and click **Manage collaborators** (it opens `โฆ/settings/collaborators`).
28
28
2. Enter the user's exact **username** (the handle used in their profile URLs โ there is
29
29
no search or autocomplete) and click **Add**.
30
30
MODIFY
docs/users/mirrors.md
+3 -2
@@ -8,8 +8,9 @@
8
8
9
9
## Adding a mirror
10
10
11
-Open your repository's overview page. As the owner you'll find a **Push mirrors** panel
12
-above the danger zone. Enter the remote URL and pick how git-shark should authenticate:
11
+Open your repository's **Settings** page (the โ Settings tab in the left sidebar, owner
12
+only). You'll find a **Push mirrors** panel above the danger zone. Enter the remote URL
13
+and pick how git-shark should authenticate:
13
14
14
15
### HTTPS remote (`https://โฆ`)
15
16
MODIFY
src/main/java/de/workaround/web/MirrorResource.java
+5 -5
@@ -65,7 +65,7 @@
65
65
{
66
66
return badRequest(e.getMessage());
67
67
}
68
- return backToRepository(owner, name);
68
+ return backToSettings(owner, name);
69
69
}
70
70
71
71
@POST
@@ -75,7 +75,7 @@
75
75
{
76
76
Owned owned = requireOwned(owner, name);
77
77
mirrorService.delete(owned.user(), owned.repository(), mirrorId);
78
- return backToRepository(owner, name);
78
+ return backToSettings(owner, name);
79
79
}
80
80
81
81
@POST
@@ -85,7 +85,7 @@
85
85
{
86
86
Owned owned = requireOwned(owner, name);
87
87
mirrorService.pushNow(owned.user(), owned.repository(), mirrorId);
88
- return backToRepository(owner, name);
88
+ return backToSettings(owner, name);
89
89
}
90
90
91
91
private record Owned(User user, Repository repository)
@@ -104,9 +104,9 @@
104
104
return new Owned(user, repository);
105
105
}
106
106
107
- private static Response backToRepository(String owner, String name)
107
+ private static Response backToSettings(String owner, String name)
108
108
{
109
- return Response.seeOther(URI.create("/repos/" + owner + "/" + name)).build();
109
+ return Response.seeOther(URI.create("/repos/" + owner + "/" + name + "/settings")).build();
110
110
}
111
111
112
112
private static Response badRequest(String message)
MODIFY
src/main/java/de/workaround/web/RepositoryResource.java
+13 -12
@@ -52,9 +52,9 @@
52
52
@CheckedTemplate
53
53
static class Templates
54
54
{
55
- static native TemplateInstance overview(Repository repo, RepoNav nav, boolean owner,
55
+ static native TemplateInstance overview(Repository repo, RepoNav nav,
56
56
List<GitBrowseService.TreeEntry> entries, GitBrowseService.CommitInfo latestCommit, String latestCommitAge,
57
- String readmeName, String readmeHtml, List<de.workaround.model.PushMirror> mirrors);
57
+ String readmeName, String readmeHtml);
58
58
59
59
static native TemplateInstance tree(Repository repo, RepoNav nav, String ref, String path,
60
60
List<GitBrowseService.TreeEntry> entries, List<Crumb> crumbs);
@@ -71,7 +71,8 @@
71
71
72
72
static native TemplateInstance tags(Repository repo, RepoNav nav, List<String> tags);
73
73
74
- static native TemplateInstance settings(Repository repo, RepoNav nav, String error);
74
+ static native TemplateInstance settings(Repository repo, RepoNav nav, String error,
75
+ List<de.workaround.model.PushMirror> mirrors);
75
76
}
76
77
77
78
@Inject
@@ -110,8 +111,6 @@
110
111
List<GitBrowseService.TreeEntry> entries = nav.empty()
111
112
? List.of()
112
113
: browse.listTree(path, nav.defaultBranch(), "").orElse(List.of());
113
- User user = currentUser.get();
114
- boolean isOwner = accessPolicy.canAdmin(user, repo);
115
114
GitBrowseService.CommitInfo latestCommit = nav.empty() ? null
116
115
: browse.commits(path, nav.defaultBranch(), 0, 1)
117
116
.filter(commitPage -> !commitPage.commits().isEmpty())
@@ -124,9 +123,7 @@
124
123
.filter(blob -> !blob.binary())
125
124
.map(blob -> Markdown.render(new String(blob.content(), StandardCharsets.UTF_8)))
126
125
.orElse(null);
127
- List<de.workaround.model.PushMirror> mirrors = isOwner ? mirrorService.list(repo) : List.of();
128
- return Templates.overview(repo, nav, isOwner, entries, latestCommit, latestCommitAge, readmeName, readmeHtml,
129
- mirrors);
126
+ return Templates.overview(repo, nav, entries, latestCommit, latestCommitAge, readmeName, readmeHtml);
130
127
}
131
128
132
129
// README file names the overview looks for, in order of preference (matched case-insensitively).
@@ -263,7 +260,7 @@
263
260
public TemplateInstance settings(@PathParam("owner") String owner, @PathParam("name") String name)
264
261
{
265
262
Repository repo = requireOwner(owner, name);
266
- return Templates.settings(repo, repoNav.build(repo, uriInfo), null);
263
+ return Templates.settings(repo, repoNav.build(repo, uriInfo), null, mirrorService.list(repo));
267
264
}
268
265
269
266
@POST
@@ -276,7 +273,9 @@
276
273
if (image == null)
277
274
{
278
275
return Response.status(Response.Status.BAD_REQUEST)
279
- .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), "No image was uploaded.")).build();
276
+ .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), "No image was uploaded.",
277
+ mirrorService.list(repo)))
278
+ .build();
280
279
}
281
280
try
282
281
{
@@ -286,7 +285,9 @@
286
285
catch (InvalidImageException e)
287
286
{
288
287
return Response.status(Response.Status.BAD_REQUEST)
289
- .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), e.getMessage())).build();
288
+ .entity(Templates.settings(repo, repoNav.build(repo, uriInfo), e.getMessage(),
289
+ mirrorService.list(repo)))
290
+ .build();
290
291
}
291
292
catch (IOException e)
292
293
{
@@ -310,7 +311,7 @@
310
311
{
311
312
return Response.status(Response.Status.BAD_REQUEST)
312
313
.entity(Templates.settings(repo, repoNav.build(repo, uriInfo),
313
- "Unknown visibility \"" + visibility + "\"."))
314
+ "Unknown visibility \"" + visibility + "\".", mirrorService.list(repo)))
314
315
.build();
315
316
}
316
317
service.changeVisibility(currentUser.require(), repo, parsed);
MODIFY
src/main/resources/templates/CollaboratorResource/collaborators.html
+2 -1
@@ -1,8 +1,9 @@
1
1
{#include layout}
2
2
{#title}Collaborators โ {repo.name}{/title}
3
3
<div class="repo-layout">
4
- {#include RepositoryResource/sidebar nav=nav active='collaborators' /}
4
+ {#include RepositoryResource/sidebar nav=nav active='settings' /}
5
5
<section class="repo-main">
6
+ <p><a href="/repos/{repo.ownerHandle}/{repo.name}/settings">โ Settings</a></p>
6
7
<h2>Collaborators</h2>
7
8
<p>Collaborators can read and push to this repository, including when it is private. Only you can manage them.</p>
8
9
{#if error}
MODIFY
src/main/resources/templates/RepositoryResource/overview.html
+0 -62
@@ -45,68 +45,6 @@
45
45
</div>
46
46
{/if}
47
47
{/if}
48
-
49
- {#if owner}
50
- <div class="panel mirrors">
51
- <div class="mirrors-head">Push mirrors</div>
52
- {#if mirrors}
53
- <ul class="mirror-list">
54
- {#for m in mirrors}
55
- <li class="mirror-row">
56
- <div class="mirror-url"><code>{m.remoteUrl}</code> <span class="tag">{m.authType.name()}</span></div>
57
- <div class="mirror-status meta">
58
- {#if m.lastError}
59
- <span class="mirror-error">Last attempt failed: {m.lastError}</span>
60
- {#else if m.lastSuccessAt}
61
- <span>Last synced {m.lastSuccessAt}</span>
62
- {#else}
63
- <span>Not synced yet</span>
64
- {/if}
65
- {#if m.lastAttemptAt}<span> ยท last attempt {m.lastAttemptAt}</span>{/if}
66
- </div>
67
- {#if m.publicKey}
68
- <div class="mirror-key">
69
- <p class="meta">Add this deploy key (with write access) at the remote:</p>
70
- <div class="mirror-deploy-key">
71
- <code>{m.publicKey}</code>
72
- <button type="button" class="btn-icon copy-btn" data-copy="{m.publicKey}" aria-label="Copy deploy key" title="Copy">โง</button>
73
- </div>
74
- </div>
75
- {/if}
76
- <div class="mirror-actions">
77
- <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/push">
78
- <button class="btn">Push now</button>
79
- </form>
80
- <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/delete">
81
- <button class="btn btn-danger">Delete</button>
82
- </form>
83
- </div>
84
- </li>
85
- {/for}
86
- </ul>
87
- {/if}
88
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors" class="mirror-add">
89
- <p><label>Remote URL <input name="url" placeholder="https://host/owner/repo.git or ssh://git@host/owner/repo.git" required></label></p>
90
- <p><label>Authentication
91
- <select name="authType">
92
- <option value="HTTPS">HTTPS โ username + password/token</option>
93
- <option value="SSH">SSH โ generated deploy key</option>
94
- </select>
95
- </label></p>
96
- <p><label>Username (HTTPS only) <input name="username" autocomplete="off"></label></p>
97
- <p><label>Password / token (HTTPS only) <input type="password" name="secret" autocomplete="new-password"></label></p>
98
- <button class="btn">Add mirror</button>
99
- </form>
100
- </div>
101
-
102
- <div class="danger-zone">
103
- <h2>Danger zone</h2>
104
- <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/delete">
105
- <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
106
- <button class="btn btn-danger">Delete repository</button>
107
- </form>
108
- </div>
109
- {/if}
110
48
</section>
111
49
112
50
</div>
MODIFY
src/main/resources/templates/RepositoryResource/settings.html
+64 -0
@@ -42,6 +42,70 @@
42
42
<button class="btn btn-danger">Remove image</button>
43
43
</form>
44
44
{/if}
45
+
46
+ <h2>Collaborators</h2>
47
+ <p class="muted">Grant other users read and write access to this repository, including when it is private.</p>
48
+ <p><a class="btn btn-secondary" href="/repos/{repo.ownerHandle}/{repo.name}/settings/collaborators">Manage collaborators</a></p>
49
+
50
+ <div class="panel mirrors">
51
+ <div class="mirrors-head">Push mirrors</div>
52
+ {#if mirrors}
53
+ <ul class="mirror-list">
54
+ {#for m in mirrors}
55
+ <li class="mirror-row">
56
+ <div class="mirror-url"><code>{m.remoteUrl}</code> <span class="tag">{m.authType.name()}</span></div>
57
+ <div class="mirror-status meta">
58
+ {#if m.lastError}
59
+ <span class="mirror-error">Last attempt failed: {m.lastError}</span>
60
+ {#else if m.lastSuccessAt}
61
+ <span>Last synced {m.lastSuccessAt}</span>
62
+ {#else}
63
+ <span>Not synced yet</span>
64
+ {/if}
65
+ {#if m.lastAttemptAt}<span> ยท last attempt {m.lastAttemptAt}</span>{/if}
66
+ </div>
67
+ {#if m.publicKey}
68
+ <div class="mirror-key">
69
+ <p class="meta">Add this deploy key (with write access) at the remote:</p>
70
+ <div class="mirror-deploy-key">
71
+ <code>{m.publicKey}</code>
72
+ <button type="button" class="btn-icon copy-btn" data-copy="{m.publicKey}" aria-label="Copy deploy key" title="Copy">โง</button>
73
+ </div>
74
+ </div>
75
+ {/if}
76
+ <div class="mirror-actions">
77
+ <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/push">
78
+ <button class="btn">Push now</button>
79
+ </form>
80
+ <form class="inline" method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors/{m.id}/delete">
81
+ <button class="btn btn-danger">Delete</button>
82
+ </form>
83
+ </div>
84
+ </li>
85
+ {/for}
86
+ </ul>
87
+ {/if}
88
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/mirrors" class="mirror-add">
89
+ <p><label>Remote URL <input name="url" placeholder="https://host/owner/repo.git or ssh://git@host/owner/repo.git" required></label></p>
90
+ <p><label>Authentication
91
+ <select name="authType">
92
+ <option value="HTTPS">HTTPS โ username + password/token</option>
93
+ <option value="SSH">SSH โ generated deploy key</option>
94
+ </select>
95
+ </label></p>
96
+ <p><label>Username (HTTPS only) <input name="username" autocomplete="off"></label></p>
97
+ <p><label>Password / token (HTTPS only) <input type="password" name="secret" autocomplete="new-password"></label></p>
98
+ <button class="btn">Add mirror</button>
99
+ </form>
100
+ </div>
101
+
102
+ <div class="danger-zone">
103
+ <h2>Danger zone</h2>
104
+ <form method="post" action="/repos/{repo.ownerHandle}/{repo.name}/delete">
105
+ <p><label>Type the repository name to confirm deletion <input name="confirm"></label></p>
106
+ <button class="btn btn-danger">Delete repository</button>
107
+ </form>
108
+ </div>
45
109
</section>
46
110
47
111
</div>
MODIFY
src/main/resources/templates/RepositoryResource/sidebar.html
+0 -1
@@ -30,7 +30,6 @@
30
30
<a class="{#if active == 'merge-requests'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/merge-requests"><span class="g">โ</span> Merge requests <span class="ct">{nav.openMrCount}</span></a>
31
31
{#if nav.isOwner}
32
32
<a class="{#if active == 'settings'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/settings"><span class="g">โ</span> Settings</a>
33
- <a class="{#if active == 'collaborators'}active{/if}" href="/repos/{nav.repo.ownerHandle}/{nav.repo.name}/settings/collaborators"><span class="g">โ</span> Collaborators</a>
34
33
{/if}
35
34
</nav>
36
35
</aside>
MODIFY
src/test/java/de/workaround/mirror/MirrorSettingsTest.java
+27 -2
@@ -68,7 +68,7 @@
68
68
assertEquals(PushMirror.AuthType.HTTPS, created.get(0).authType);
69
69
70
70
String page = given()
71
- .when().get("/repos/" + OWNER + "/" + repo.name)
71
+ .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
72
72
.then().statusCode(200)
73
73
.extract().asString();
74
74
assertTrue(page.contains("https://mirror-target.example/owner/repo.git"), "mirror must be listed");
@@ -111,7 +111,7 @@
111
111
assertTrue(mirror.publicKey.startsWith("ssh-ed25519 "), "public key must be OpenSSH Ed25519");
112
112
113
113
given()
114
- .when().get("/repos/" + OWNER + "/" + repo.name)
114
+ .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
115
115
.then().statusCode(200)
116
116
.body(containsString("ssh-ed25519 "))
117
117
.body(not(containsString("BEGIN PRIVATE KEY")));
@@ -214,10 +214,35 @@
214
214
Repository repo = repoOwnedBy(OWNER, "settings-hidden");
215
215
createMirror(repo);
216
216
217
+ // mirrors live on the owner-only settings page; the repo settings route hides its existence
218
+ // from non-owners (404), and the public overview shows nothing
217
219
given()
218
220
.when().get("/repos/" + OWNER + "/" + repo.name)
219
221
.then().statusCode(200)
220
222
.body(not(containsString("Push mirrors")));
223
+
224
+ given()
225
+ .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
226
+ .then().statusCode(404);
227
+ }
228
+
229
+ @Test
230
+ @TestSecurity(user = OWNER)
231
+ void mirrorsAndDangerZoneLiveUnderSettingsNotOverview()
232
+ {
233
+ Repository repo = ownedRepo("settings-relocated");
234
+
235
+ given()
236
+ .when().get("/repos/" + OWNER + "/" + repo.name)
237
+ .then().statusCode(200)
238
+ .body(not(containsString("Push mirrors")))
239
+ .body(not(containsString("Danger zone")));
240
+
241
+ given()
242
+ .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
243
+ .then().statusCode(200)
244
+ .body(containsString("Push mirrors"))
245
+ .body(containsString("Danger zone"));
221
246
}
222
247
223
248
private Repository ownedRepo(String name)
MODIFY
src/test/java/de/workaround/web/CollaboratorSettingsTest.java
+8 -1
@@ -219,13 +219,20 @@
219
219
220
220
@Test
221
221
@TestSecurity(user = OWNER)
222
- void ownerSeesSettingsLinkInSidebar()
222
+ void collaboratorsAreHiddenBehindSettingsTabNotTheSidebar()
223
223
{
224
224
Repository repo = repoOwnedBy(OWNER, "collab-nav", Repository.Visibility.PUBLIC);
225
225
226
+ // the sidebar only offers the Settings tab; collaborators is reached from the settings page
226
227
given()
227
228
.when().get("/repos/" + OWNER + "/" + repo.name)
228
229
.then().statusCode(200)
230
+ .body(containsString("/settings\""))
231
+ .body(not(containsString("settings/collaborators")));
232
+
233
+ given()
234
+ .when().get("/repos/" + OWNER + "/" + repo.name + "/settings")
235
+ .then().statusCode(200)
229
236
.body(containsString("settings/collaborators"));
230
237
}
231
238