Public: anyone can read this; the crew can change it.

The app page says who the app answers to, including a private app on a public repository #2591

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2568-the-app-page-says-who-the-app-answers-to → main 1 commit fork

  • Merged. scout/2568-the-app-page-says-who-the-app-answers-to was merged into main as 56c080d.

Review

Sign in to review this change.
2 files changed +95 −0
src/web/studio/previews.ts +6
@@ -7,6 +7,7 @@
77 * deploy event refreshes (`appStateFragment`) share.
88 */
99
10 import { audienceFacts, audienceOf } from "../../core/audience.ts";
1011 import { sealedScope } from "../../core/sealed.ts";
1112 import { terminalFor } from "../../actions/apps.ts";
1213 import { envPull, previewEnv } from "../../apps/data.ts";
@@ -298,6 +299,10 @@ export function appPanels(
298299 // The state in one word, and under it the facts — why it is not up, and
299300 // the deploy whose log says the rest.
300301 const live = envPhrase(production);
302 // Who the app answers to, in the sentence the forge's own rules produce.
303 const audience = audienceOf(
304 audienceFacts(db, repo, ctx.auth.user),
305 ).sentence;
301306 const head = html`<div class="page-head studio-head">
302307 <div class="stack stack-tight">
303308 <h1>
@@ -320,6 +325,7 @@ export function appPanels(
320325 >`
321326 : html`· <span class="mono">${url.replace(/^https?:\/\//, "")}</span>`}
322327 </p>
328 <p class="muted small" id="app-audience">${audience}</p>
323329 ${when(production, (env) => envFacts(base, env))}
324330 ${when(writer, () =>
325331 terminalLine(
test/app-audience.test.ts added +89
@@ -0,0 +1,89 @@
1 /**
2 * The app page says who the app answers to, in the head beside its state:
3 * a public app, a private app, and a private app on a public repository
4 * (#620).
5 */
6 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { newApp } from "../src/actions/apps.ts";
11 import { updateApp } from "../src/core/apps.ts";
12 import { createApp, type App } from "../src/app.ts";
13 import { createUser, type User } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 describe("the app page's audience sentence", () => {
20 let dataDir: string;
21 let app: App;
22 let servers: Servers;
23 let alice: User;
24 let cookie = "";
25
26 const sentence = async (name: string, withCookie = cookie) => {
27 const res = await fetch(`${servers.url}/alice/${name}/app`, {
28 headers: withCookie ? { cookie: withCookie } : {},
29 });
30 expect(res.status).toBe(200);
31 const body = await res.text();
32 return /<p class="muted small" id="app-audience">([^<]*)<\/p>/.exec(body)?.[1];
33 };
34
35 beforeAll(async () => {
36 dataDir = await mkdtemp(join(tmpdir(), "forge-app-audience-"));
37 app = createApp({ dataDir, port: 0, host: "forge.test" });
38 servers = await serve(app);
39 const made = await createUser(app.db, {
40 origin: "seed",
41 trusted: true,
42 username: "alice",
43 email: "[email protected]",
44 password: "a-good-passphrase",
45 adminIfFirst: true,
46 });
47 if (!made.ok) throw new Error(made.error);
48 alice = made.value;
49 for (const [name, visibility, access] of [
50 ["open", "public", "public"],
51 ["closed", "private", "private"],
52 ["split", "public", "private"],
53 ] as const) {
54 const r = await newApp(app, alice, { name, description: "x", visibility });
55 if (!r.ok) throw new Error(String(r.error));
56 updateApp(app.db, r.value.app.id, { access });
57 }
58 const res = await fetch(`${servers.url}/signin`, {
59 method: "POST",
60 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
61 redirect: "manual",
62 });
63 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0]!;
64 }, 120_000);
65
66 afterAll(async () => {
67 servers.stop();
68 app.close();
69 await rm(dataDir, { recursive: true, force: true });
70 });
71
72 test("a public app says anyone can read it", async () => {
73 expect(await sentence("open")).toStartWith("Public: anyone can read this;");
74 });
75
76 test("a private app says it is private", async () => {
77 expect(await sentence("closed")).toStartWith("Private: only you");
78 });
79
80 test("a private app on a public repository says the code is public (#620)", async () => {
81 expect(await sentence("split")).toStartWith(
82 "The code is public; the app answers only to you",
83 );
84 // Signed out, the page is seen as a stranger: never told "you".
85 const out = await sentence("split", "");
86 expect(out).toStartWith("The code is public; the app answers only to ");
87 expect(out).not.toContain("you");
88 });
89 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs