mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
desktop: Open releases on Delegates
- Decide in ADR 0004 to always open releases on Delegates - Drop the fallback to Others in lists and releases - Show the Others filter whenever it has content - Name the scope in the empty artifacts message - Open Others in the e2e test of an artifact by a non-delegate, which no longer opens there by itself
This commit is contained in:
@@ -30,14 +30,13 @@ impl ReleaseFilter {
|
||||
pub trait Releases: Profile {
|
||||
/// List a repository's releases, newest first.
|
||||
///
|
||||
/// Scoped to releases created by a delegate and artifacts authored by a
|
||||
/// delegate, or with an untrusted `filter` to those by non-delegates, so
|
||||
/// the two scopes never overlap. Artifacts redacted by a trusted party are
|
||||
/// hidden unless `filter` shows them. A release with no artifact in scope
|
||||
/// shows every author's. A release whose artifacts were all redacted is
|
||||
/// hidden with them; a release with no artifacts is shown. This is the view
|
||||
/// `rad-artifact list` gives. Without `take` the full list is returned and
|
||||
/// `skip` is ignored.
|
||||
/// Scoped to releases created by a delegate, or with an untrusted `filter`
|
||||
/// to those by non-delegates, so the two scopes never overlap. Each release
|
||||
/// lists only the artifacts authored by a delegate, in either scope.
|
||||
/// Artifacts redacted by a trusted party are hidden unless `filter` shows
|
||||
/// them. A release whose artifacts were all redacted is hidden with them; a
|
||||
/// release with no artifacts is shown. Without `take` the full list is
|
||||
/// returned and `skip` is ignored.
|
||||
fn list_releases(
|
||||
&self,
|
||||
rid: identity::RepoId,
|
||||
@@ -60,19 +59,14 @@ pub trait Releases: Profile {
|
||||
.filter_map(Result::ok)
|
||||
.filter(|(_, release)| filters.shows_release(release));
|
||||
|
||||
// With no artifact by an author in scope, fall back to every author, as
|
||||
// the release page does, so a teaser never counts fewer artifacts than
|
||||
// its page lists. A redacted artifact in scope keeps the scope.
|
||||
let fallback = Filters {
|
||||
scope: Scope::All,
|
||||
// A release page opens on the delegates' artifacts, so a teaser counts
|
||||
// those in either scope.
|
||||
let artifact_filters = Filters {
|
||||
scope: Scope::Trusted,
|
||||
..filters
|
||||
};
|
||||
let summary = |(id, release): (radicle::cob::ObjectId, radicle_artifact::Release)| {
|
||||
let has_trusted = release
|
||||
.artifacts()
|
||||
.values()
|
||||
.any(|artifact| filters.trust.admits(filters.scope, artifact.author()));
|
||||
let artifacts = if has_trusted { filters } else { fallback }
|
||||
let artifacts = artifact_filters
|
||||
.artifacts(&release)
|
||||
.map(|(cid, artifact)| {
|
||||
cobs::release::Artifact::new(cid, artifact, store.delegates(), &aliases)
|
||||
@@ -195,8 +189,8 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_falls_back_to_every_author_without_delegate_artifact() {
|
||||
assert_eq!(listed_artifacts(|_, _| {}), 1);
|
||||
fn list_never_widens_to_non_delegate_artifact() {
|
||||
assert_eq!(listed_artifacts(|_, _| {}), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -293,6 +287,40 @@ mod test {
|
||||
assert_eq!(listed(ReleaseScope::Untrusted), [theirs]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_others_releases_with_delegate_artifacts() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let profile = test::profile(&tmp.path().join("home"), [0xff; 32]);
|
||||
let signer = SigningKey::from_seed(Seed::new([0xff; 32]));
|
||||
let other = SigningKey::from_seed(Seed::new([0xee; 32]));
|
||||
let (rid, _, _, head) =
|
||||
fixtures::project(tmp.path().join("project"), &profile.storage, &signer).unwrap();
|
||||
{
|
||||
let repo = profile.storage.repository(rid).unwrap();
|
||||
let mut releases = ArtifactStore::open(&repo).unwrap();
|
||||
let mut theirs = releases
|
||||
.create(radicle::git::Oid::from(head), None, &other)
|
||||
.unwrap();
|
||||
theirs
|
||||
.register_artifact(Cid::from_str(CID).unwrap(), "bin".into(), &signer)
|
||||
.unwrap();
|
||||
theirs
|
||||
.register_artifact(Cid::from_str(OTHER_CID).unwrap(), "eve".into(), &other)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let filter = ReleaseFilter {
|
||||
scope: ReleaseScope::Untrusted,
|
||||
show_redacted: false,
|
||||
};
|
||||
let list = AppState { profile }
|
||||
.list_releases(rid, Some(filter), None, None)
|
||||
.unwrap();
|
||||
let artifacts = &list.content[0].artifacts;
|
||||
assert_eq!(artifacts.len(), 1);
|
||||
assert_eq!(artifacts[0].name, "bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_counts_split_delegates_from_others() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -53,11 +53,18 @@ without alarm. It shows:
|
||||
counts delegates only, as anyone can make keys that attest their own artifact.
|
||||
The full list still shows every attestation.
|
||||
|
||||
**Keep untrusted content visible.** Others' releases and artifacts stay listed
|
||||
by default, behind their own filter.
|
||||
**Open on Delegates, and never switch scope by itself.** The releases list and
|
||||
the artifacts of a release always open on Delegates, also when Delegates is
|
||||
empty. The Others filter shows whenever Others has content, so untrusted content
|
||||
is one deliberate click away. An empty Delegates scope tells the user that the
|
||||
delegates published nothing. Opening on Others would hide that, and would show
|
||||
untrusted content the user did not ask for.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Open on Others when Delegates is empty.** This avoids an empty page, but
|
||||
shows untrusted content without the user asking for it, and hides that the
|
||||
delegates published nothing.
|
||||
- **Hide Others by default**, behind a global setting. This protects users best,
|
||||
and avoids showing links to malware at all, which is a reputation risk a
|
||||
warning doesn't remove. It also hides legitimate contributions, such as builds
|
||||
@@ -77,4 +84,6 @@ by default, behind their own filter.
|
||||
its title, tag and commit are not trusted.
|
||||
- Delegation is the only trust signal. Release managers who are not delegates
|
||||
show as Others until releases can use canonical ref rules.
|
||||
- radicle-explorer should show the same warnings so that both clients agree.
|
||||
- An empty Delegates scope shows an empty page, also when Others has content.
|
||||
- radicle-explorer shows the same warnings and opens on the same scope, so that
|
||||
both clients agree.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Release } from "@bindings/cob/release/Release";
|
||||
import type { ReleaseScope } from "@bindings/cob/release/ReleaseScope";
|
||||
|
||||
import { push } from "@app/lib/router";
|
||||
import {
|
||||
@@ -16,13 +15,12 @@
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
|
||||
interface Props {
|
||||
scope: ReleaseScope;
|
||||
delegateIds: Set<string>;
|
||||
release: Release;
|
||||
rid: string;
|
||||
}
|
||||
|
||||
const { scope, delegateIds, release, rid }: Props = $props();
|
||||
const { delegateIds, release, rid }: Props = $props();
|
||||
|
||||
const title = $derived(release.title || release.tagName || release.id);
|
||||
</script>
|
||||
@@ -79,7 +77,6 @@
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: release.id,
|
||||
scope,
|
||||
});
|
||||
}}>
|
||||
<div class="global-chip icon">
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
import type { Author } from "@bindings/cob/Author";
|
||||
import type { Artifact } from "@bindings/cob/release/Artifact";
|
||||
import type { Location } from "@bindings/cob/release/Location";
|
||||
import type { ReleaseCounts } from "@bindings/cob/release/ReleaseCounts";
|
||||
import type { ReleaseScope } from "@bindings/cob/release/ReleaseScope";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
|
||||
// With no scope asked for and no delegate releases, open on the untrusted
|
||||
// scope rather than on an empty list.
|
||||
export function releaseListScope(
|
||||
requested: ReleaseScope | undefined,
|
||||
counts: ReleaseCounts,
|
||||
): ReleaseScope {
|
||||
if (requested !== undefined) {
|
||||
return requested;
|
||||
}
|
||||
return counts.delegate === 0 && counts.other > 0 ? "untrusted" : "trusted";
|
||||
}
|
||||
|
||||
export interface ArtifactView {
|
||||
scope: ReleaseScope;
|
||||
shown: Artifact[];
|
||||
redactedCount: number;
|
||||
counts: Record<ReleaseScope, number>;
|
||||
showFilters: boolean;
|
||||
}
|
||||
|
||||
// Falls back to the other scope when the one asked for is empty.
|
||||
// Never falls back to the other scope; see docs/adr/0004.
|
||||
export function artifactView(
|
||||
artifacts: Artifact[],
|
||||
delegates: Set<string>,
|
||||
wanted: ReleaseScope,
|
||||
scope: ReleaseScope,
|
||||
showRedacted: boolean,
|
||||
): ArtifactView {
|
||||
const byScope: Record<ReleaseScope, Artifact[]> = {
|
||||
@@ -38,18 +24,15 @@ export function artifactView(
|
||||
};
|
||||
const visible = (list: Artifact[]) =>
|
||||
showRedacted ? list : list.filter(a => !a.redacted);
|
||||
const other = wanted === "trusted" ? "untrusted" : "trusted";
|
||||
const scope = byScope[wanted].length > 0 ? wanted : other;
|
||||
|
||||
return {
|
||||
scope,
|
||||
shown: visible(byScope[scope]),
|
||||
redactedCount: byScope[scope].filter(a => a.redacted).length,
|
||||
counts: {
|
||||
trusted: visible(byScope.trusted).length,
|
||||
untrusted: visible(byScope.untrusted).length,
|
||||
},
|
||||
showFilters: byScope.trusted.length > 0 && byScope.untrusted.length > 0,
|
||||
showFilters: scope === "untrusted" || byScope.untrusted.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,31 +36,29 @@
|
||||
config: Config;
|
||||
release: Release;
|
||||
scope?: ReleaseScope;
|
||||
artifactScope?: ReleaseScope;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { repo, config, release, scope, artifactScope }: Props = $props();
|
||||
let { repo, config, release, scope }: Props = $props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
const ownDid = $derived(didFromPublicKey(config.publicKey));
|
||||
|
||||
const title = $derived(release.title || release.tagName || release.id);
|
||||
const delegateIds = $derived(new Set(repo.delegates.map(d => d.did)));
|
||||
const listScope: ReleaseScope = $derived(
|
||||
delegateIds.has(release.creator.did) ? "trusted" : "untrusted",
|
||||
);
|
||||
|
||||
let showRedacted = $state(false);
|
||||
|
||||
const artifactScope: ReleaseScope = $derived(scope ?? "trusted");
|
||||
const view = $derived(
|
||||
artifactView(
|
||||
release.artifacts,
|
||||
delegateIds,
|
||||
artifactScope ?? "trusted",
|
||||
showRedacted,
|
||||
),
|
||||
artifactView(release.artifacts, delegateIds, artifactScope, showRedacted),
|
||||
);
|
||||
|
||||
const warning = $derived(
|
||||
releaseWarning(release.creator.did, delegateIds, view.scope),
|
||||
releaseWarning(release.creator.did, delegateIds, artifactScope),
|
||||
);
|
||||
const untrustedWarning = $derived(
|
||||
warning === "release"
|
||||
@@ -355,19 +353,17 @@
|
||||
Releases
|
||||
</button>
|
||||
<Icon name="chevron-right" />
|
||||
{#if scope}
|
||||
<button
|
||||
class="breadcrumb-link"
|
||||
onclick={() =>
|
||||
router.push({
|
||||
resource: "repo.releases",
|
||||
rid: repo.rid,
|
||||
scope,
|
||||
})}>
|
||||
{scope === "trusted" ? "Delegates" : "Others"}
|
||||
</button>
|
||||
<Icon name="chevron-right" />
|
||||
{/if}
|
||||
<button
|
||||
class="breadcrumb-link"
|
||||
onclick={() =>
|
||||
router.push({
|
||||
resource: "repo.releases",
|
||||
rid: repo.rid,
|
||||
scope: listScope,
|
||||
})}>
|
||||
{listScope === "trusted" ? "Delegates" : "Others"}
|
||||
</button>
|
||||
<Icon name="chevron-right" />
|
||||
<Id id={release.id} clipboard={release.id} label="release ID" />
|
||||
</div>
|
||||
<div style:margin-left="auto" style:display="flex" style:gap="0.5rem">
|
||||
@@ -440,13 +436,12 @@
|
||||
styleHeight="1.75rem"
|
||||
bordered
|
||||
flatRight
|
||||
active={view.scope === "trusted"}
|
||||
active={artifactScope === "trusted"}
|
||||
onclick={() =>
|
||||
router.push({
|
||||
resource: "repo.release",
|
||||
rid: repo.rid,
|
||||
release: release.id,
|
||||
scope,
|
||||
})}>
|
||||
<Icon name="badge" />Delegates
|
||||
<span class="global-counter-badge">{view.counts.trusted}</span>
|
||||
@@ -455,15 +450,14 @@
|
||||
styleHeight="1.75rem"
|
||||
bordered
|
||||
flatLeft
|
||||
active={view.scope === "untrusted"}
|
||||
active={artifactScope === "untrusted"}
|
||||
title="Non-delegates"
|
||||
onclick={() =>
|
||||
router.push({
|
||||
resource: "repo.release",
|
||||
rid: repo.rid,
|
||||
release: release.id,
|
||||
scope,
|
||||
artifactScope: "untrusted",
|
||||
scope: "untrusted",
|
||||
})}>
|
||||
<Icon name="avatar-incognito" />Others
|
||||
<span class="global-counter-badge">
|
||||
@@ -498,7 +492,11 @@
|
||||
{#if view.shown.length === 0}
|
||||
<div class="empty-artifacts">
|
||||
<Icon name="attach" />
|
||||
No artifacts
|
||||
{!view.showFilters
|
||||
? "No artifacts"
|
||||
: artifactScope === "trusted"
|
||||
? "No artifacts by delegates"
|
||||
: "No artifacts by others"}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
onState={list.persistScroll}>
|
||||
{#snippet row(release)}
|
||||
<div class="row">
|
||||
<ReleaseTeaser {release} {scope} {delegateIds} rid={repo.rid} />
|
||||
<ReleaseTeaser {release} {delegateIds} rid={repo.rid} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</VirtualList>
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
cachedGetDiffText,
|
||||
invoke,
|
||||
} from "@app/lib/invoke";
|
||||
import { releaseListScope } from "@app/lib/releases";
|
||||
import type { LoadedRoute, SidebarData } from "@app/lib/router/definitions";
|
||||
import { loadSidebarData } from "@app/lib/router/definitions";
|
||||
import { unreachable } from "@app/lib/utils";
|
||||
@@ -215,9 +214,7 @@ export interface RepoReleaseRoute {
|
||||
resource: "repo.release";
|
||||
rid: string;
|
||||
release: string;
|
||||
// The release list the user came from, shown in the breadcrumb.
|
||||
scope?: ReleaseScope;
|
||||
artifactScope?: ReleaseScope;
|
||||
}
|
||||
|
||||
export interface LoadedRepoReleaseRoute {
|
||||
@@ -227,7 +224,6 @@ export interface LoadedRepoReleaseRoute {
|
||||
config: Config;
|
||||
release: Release;
|
||||
scope?: ReleaseScope;
|
||||
artifactScope?: ReleaseScope;
|
||||
sidebarData: SidebarData;
|
||||
};
|
||||
}
|
||||
@@ -570,8 +566,8 @@ export function listReleases(
|
||||
export async function loadReleases(
|
||||
route: RepoReleasesRoute,
|
||||
): Promise<LoadedRepoReleasesRoute> {
|
||||
let scope = route.scope ?? "trusted";
|
||||
const [sidebarData, repo, firstPage, releaseCounts] = await Promise.all([
|
||||
const scope = route.scope ?? "trusted";
|
||||
const [sidebarData, repo, releases, releaseCounts] = await Promise.all([
|
||||
loadSidebarData(),
|
||||
invoke<RepoInfo>("repo_by_id", {
|
||||
rid: route.rid,
|
||||
@@ -581,15 +577,9 @@ export async function loadReleases(
|
||||
rid: route.rid,
|
||||
}),
|
||||
]);
|
||||
let releases = firstPage;
|
||||
|
||||
const fallback = releaseListScope(route.scope, releaseCounts);
|
||||
if (fallback !== scope) {
|
||||
scope = fallback;
|
||||
releases = await listReleases(route.rid, scope, 0, RELEASES_PER_PAGE);
|
||||
}
|
||||
|
||||
const showFilters = releaseCounts.delegate > 0 && releaseCounts.other > 0;
|
||||
// Others is one click away whenever it has releases, and Delegates is one
|
||||
// click back from it; see docs/adr/0004.
|
||||
const showFilters = scope === "untrusted" || releaseCounts.other > 0;
|
||||
|
||||
return {
|
||||
resource: "repo.releases",
|
||||
@@ -623,7 +613,6 @@ export async function loadRelease(
|
||||
return loadReleases({
|
||||
resource: "repo.releases",
|
||||
rid: route.rid,
|
||||
scope: route.scope,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -635,7 +624,6 @@ export async function loadRelease(
|
||||
config: sidebarData.config,
|
||||
release,
|
||||
scope: route.scope,
|
||||
artifactScope: route.artifactScope,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -706,11 +694,6 @@ export function repoRouteToPath(route: RepoRoute): string {
|
||||
let url = [...pathSegments, "releases", route.release].join("/");
|
||||
if (route.scope) {
|
||||
searchParams.set("scope", route.scope);
|
||||
}
|
||||
if (route.artifactScope === "untrusted") {
|
||||
searchParams.set("artifacts", "untrusted");
|
||||
}
|
||||
if (searchParams.size > 0) {
|
||||
url += `?${searchParams}`;
|
||||
}
|
||||
return url;
|
||||
@@ -818,17 +801,7 @@ export function repoUrlToRoute(
|
||||
const id = segments.shift();
|
||||
const scope = parseReleaseScope(searchParams.get("scope"));
|
||||
if (id) {
|
||||
const artifactScope =
|
||||
searchParams.get("artifacts") === "untrusted"
|
||||
? "untrusted"
|
||||
: undefined;
|
||||
return {
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: id,
|
||||
scope,
|
||||
artifactScope,
|
||||
};
|
||||
return { resource: "repo.release", rid, release: id, scope };
|
||||
} else {
|
||||
return { resource: "repo.releases", rid, scope };
|
||||
}
|
||||
|
||||
@@ -310,6 +310,9 @@ test("an artifact by a non-delegate shows a warning", async ({
|
||||
});
|
||||
await reload(page);
|
||||
|
||||
// Releases always open on Delegates, see docs/adr/0004.
|
||||
await expect(page.getByText("No artifacts by delegates")).toBeVisible();
|
||||
await page.getByRole("button", { name: /Others/ }).click();
|
||||
await expect(
|
||||
page.getByRole("note").getByText("Not from delegates."),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Artifact } from "@bindings/cob/release/Artifact";
|
||||
import type { ReleaseCounts } from "@bindings/cob/release/ReleaseCounts";
|
||||
import type { Commit } from "@bindings/repo/Commit";
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
matchCommits,
|
||||
parseMetadataValue,
|
||||
redactedByDelegate,
|
||||
releaseListScope,
|
||||
releaseWarning,
|
||||
webLocations,
|
||||
} from "@app/lib/releases";
|
||||
@@ -40,27 +38,6 @@ function artifact(cid: string, overrides: Partial<Artifact> = {}): Artifact {
|
||||
};
|
||||
}
|
||||
|
||||
function counts(delegate: number, other: number): ReleaseCounts {
|
||||
return { delegate, delegateRedacted: 0, other, otherRedacted: 0 };
|
||||
}
|
||||
|
||||
describe("releaseListScope", () => {
|
||||
test("keeps a requested scope", () => {
|
||||
expect(releaseListScope("trusted", counts(0, 3))).toBe("trusted");
|
||||
});
|
||||
|
||||
test("opens on untrusted when only others released", () => {
|
||||
expect(releaseListScope(undefined, counts(0, 3))).toBe("untrusted");
|
||||
});
|
||||
|
||||
test.each([
|
||||
[1, 3],
|
||||
[0, 0],
|
||||
])("opens on trusted with %d delegate and %d other", (d, o) => {
|
||||
expect(releaseListScope(undefined, counts(d, o))).toBe("trusted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("artifactView", () => {
|
||||
const ours = artifact("ours");
|
||||
const ourRedacted = artifact("ours-redacted", { redacted: true });
|
||||
@@ -68,15 +45,24 @@ describe("artifactView", () => {
|
||||
|
||||
test("shows the wanted scope", () => {
|
||||
const view = artifactView([ours, theirs], delegates, "untrusted", false);
|
||||
expect(view.scope).toBe("untrusted");
|
||||
expect(view.shown).toEqual([theirs]);
|
||||
expect(view.showFilters).toBe(true);
|
||||
});
|
||||
|
||||
test("falls back to the other scope when the wanted one is empty", () => {
|
||||
test("keeps the wanted scope when it is empty", () => {
|
||||
const view = artifactView([theirs], delegates, "trusted", false);
|
||||
expect(view.scope).toBe("untrusted");
|
||||
expect(view.shown).toEqual([theirs]);
|
||||
expect(view.shown).toEqual([]);
|
||||
expect(view.showFilters).toBe(true);
|
||||
});
|
||||
|
||||
test("keeps the filter on others without artifacts by others", () => {
|
||||
const view = artifactView([ours], delegates, "untrusted", false);
|
||||
expect(view.shown).toEqual([]);
|
||||
expect(view.showFilters).toBe(true);
|
||||
});
|
||||
|
||||
test("hides the filter without artifacts by others", () => {
|
||||
const view = artifactView([ours], delegates, "trusted", false);
|
||||
expect(view.showFilters).toBe(false);
|
||||
});
|
||||
|
||||
@@ -103,7 +89,6 @@ describe("artifactView", () => {
|
||||
"trusted",
|
||||
false,
|
||||
);
|
||||
expect(view.scope).toBe("trusted");
|
||||
expect(view.shown).toEqual([]);
|
||||
expect(view.counts).toEqual({ trusted: 0, untrusted: 1 });
|
||||
});
|
||||
|
||||
@@ -101,27 +101,15 @@ describe("repoRouteToPath and repoUrlToRoute round trip", () => {
|
||||
rid,
|
||||
release: "abc",
|
||||
scope: undefined,
|
||||
artifactScope: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"/repos/RID/releases/abc?scope=trusted",
|
||||
{
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: "abc",
|
||||
scope: "trusted",
|
||||
artifactScope: undefined,
|
||||
},
|
||||
],
|
||||
[
|
||||
"/repos/RID/releases/abc?scope=untrusted&artifacts=untrusted",
|
||||
"/repos/RID/releases/abc?scope=untrusted",
|
||||
{
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: "abc",
|
||||
scope: "untrusted",
|
||||
artifactScope: "untrusted",
|
||||
},
|
||||
],
|
||||
])("%s", (template, route) => {
|
||||
@@ -173,27 +161,12 @@ describe("repoUrlToRoute", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("leaves the trusted artifact scope out of the URL", () => {
|
||||
expect(
|
||||
repoRouteToPath({
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: "abc",
|
||||
scope: undefined,
|
||||
artifactScope: "trusted",
|
||||
}),
|
||||
).toBe(`/repos/${rid}/releases/abc`);
|
||||
});
|
||||
|
||||
test("ignores an unknown release scope", () => {
|
||||
expect(
|
||||
parse(`/repos/${rid}/releases/abc?scope=bogus&artifacts=bogus`),
|
||||
).toEqual({
|
||||
expect(parse(`/repos/${rid}/releases/abc?scope=bogus`)).toEqual({
|
||||
resource: "repo.release",
|
||||
rid,
|
||||
release: "abc",
|
||||
scope: undefined,
|
||||
artifactScope: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user