package e2e import ( "os" "path/filepath" "strings" "testing" ) func TestOwnerProfiles(t *testing.T) { inst := startInstance(t) aliceKey := inst.newKey(t, "alice") bobKey := inst.newKey(t, "bob") inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") // Self-service user profile; website validated. if _, errOut, code := inst.ssh(t, aliceKey, "", "profile", "set", "--description", "'builds small tools'", "--website", "https://alice.example"); code != 0 { t.Fatalf("profile set: %s", errOut) } if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--website", "gopher://nope"); code != 2 { t.Fatal("bad website scheme accepted") } out, _, _ := inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json") if !strings.Contains(out, `"description":"builds small tools"`) || !strings.Contains(out, `"website":"https://alice.example"`) { t.Fatalf("profile show: %s", out) } // A profile is the whole page's worth: repositories the caller may // see, org membership, and the activity window — all previously // readable only by the web, which went straight to the store. if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/public-tool"); code != 0 { t.Fatal("repo create") } if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/secret", "--private"); code != 0 { t.Fatal("private repo create") } if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "toolmakers"); code != 0 { t.Fatal("org create") } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") for _, want := range []string{`"path":"alice/public-tool"`, `"path":"alice/secret"`, `"name":"toolmakers"`, `"activity_total"`} { if !strings.Contains(out, want) { t.Errorf("own profile missing %q: %s", want, out) } } // A profile's repository rows carry the listing metadata the web // shows: topics, license, default branch, last commit. Without them // the web had to decorate the rows itself, which is what kept it // reading the store (krz/gitbay#47). if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "topics", "add", "alice/public-tool", "cli", "go"); code != 0 { t.Fatalf("topics add: %s", errOut) } env := inst.gitEnv(aliceKey) work := t.TempDir() mustGit(t, work, env, "clone", inst.sshURL("alice/public-tool"), "w") dir := filepath.Join(work, "w") os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("MIT License\n\nPermission is hereby granted, free of charge\n"), 0o644) mustGit(t, dir, env, "checkout", "-q", "-b", "main") mustGit(t, dir, env, "add", ".") mustGit(t, dir, env, "commit", "-q", "-m", "add license") mustGit(t, dir, env, "push", "-q", "origin", "main") out, _, _ = inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json") for _, want := range []string{`"cli"`, `"go"`, `"license":"MIT"`, `"default_branch":"main"`, `"updated"`} { if !strings.Contains(out, want) { t.Errorf("profile repo row missing %q: %s", want, out) } } // The owner page renders those rows, having dispatched the same // command rather than assembling them from the store again. status, body := inst.get(t, "/alice") if status != 200 { t.Fatalf("owner page: %d", status) } for _, want := range []string{">public-tool<", "?q=cli", "MIT", "updated "} { if !strings.Contains(body, want) { t.Errorf("owner page missing %q:\n%s", want, body) } } if strings.Contains(body, "secret") { t.Fatal("owner page leaks a private repo") } // A stranger sees the public repository and not the private one. out, _, _ = inst.ssh(t, bobKey, "", "profile", "show", "alice", "--json") if !strings.Contains(out, `"path":"alice/public-tool"`) { t.Errorf("stranger cannot see a public repo on a profile: %s", out) } if strings.Contains(out, `"alice/secret"`) { t.Errorf("a private repository leaked onto a profile: %s", out) } // An org profile lists its members rather than org memberships. out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "toolmakers", "--json") if !strings.Contains(out, `"kind":"org"`) || !strings.Contains(out, `"name":"alice"`) { t.Errorf("org profile members: %s", out) } // Partial update leaves the other field untouched; empty clears. if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--description", "'tinkerer'"); code != 0 { t.Fatal("partial set failed") } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "--json") if !strings.Contains(out, "tinkerer") || !strings.Contains(out, "alice.example") { t.Fatalf("partial update clobbered website: %s", out) } if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--website", "''"); code != 0 { t.Fatal("clear failed") } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "--json") if strings.Contains(out, "alice.example") { t.Fatalf("website not cleared: %s", out) } // Org profile: admin sets, member cannot; no flags shows. if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "workshop"); code != 0 { t.Fatal("org create failed") } if _, _, code := inst.ssh(t, aliceKey, "", "org", "members", "add", "workshop", "bob"); code != 0 { t.Fatal("member add failed") } if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "profile", "workshop", "--description", "'where things get made'", "--website", "https://workshop.example"); code != 0 { t.Fatalf("org profile set: %s", errOut) } if _, _, code := inst.ssh(t, bobKey, "", "org", "profile", "workshop", "--description", "hax"); code != 4 { t.Fatal("member set org profile") } out, _, _ = inst.ssh(t, bobKey, "", "org", "profile", "workshop") if !strings.Contains(out, "where things get made") { t.Fatalf("org profile show: %s", out) } // About: long-form markdown, set inline or piped, rendered on the page. if _, errOut, code := inst.ssh(t, aliceKey, "# Hello\n\nI maintain *small tools*.\n", "profile", "set", "--file", "-"); code != 0 { t.Fatalf("about from stdin: %s", errOut) } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") if !strings.Contains(out, "I maintain *small tools*.") { t.Fatalf("about not stored: %s", out) } if !strings.Contains(out, "tinkerer") { t.Fatalf("about clobbered the description: %s", out) } // Org-mode about: the stored format picks the renderer. if _, errOut, code := inst.ssh(t, aliceKey, "* Tools\n\nI maintain /small tools/.\n", "profile", "set", "--file", "-", "--about-format", "org"); code != 0 { t.Fatalf("org about: %s", errOut) } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") if !strings.Contains(out, `"about_format":"org"`) { t.Fatalf("about format not stored: %s", out) } if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--about-format", "rst"); code != 2 { t.Fatal("unknown about format accepted") } // Org emphasis parsed, not left as literal slashes the way the // markdown renderer would. _, body = inst.get(t, "/alice") if !strings.Contains(body, "small tools") || strings.Contains(body, "/small tools/") { t.Fatalf("org about not rendered as org: %s", body) } // Back to markdown for the rest of the checks. if _, _, code := inst.ssh(t, aliceKey, "# Hello\n\nI maintain *small tools*.\n", "profile", "set", "--file", "-", "--about-format", "md"); code != 0 { t.Fatal("markdown about") } // Links: free-form, labelled or bare, capped, cleared by an empty one. if _, errOut, code := inst.ssh(t, aliceKey, "", "profile", "set", "--link", "'Mastodon|https://fosstodon.example/@alice'", "--link", "https://alice.example/now"); code != 0 { t.Fatalf("links: %s", errOut) } out, _, _ = inst.ssh(t, aliceKey, "", "profile", "show", "alice", "--json") if !strings.Contains(out, `"label":"Mastodon"`) || !strings.Contains(out, `"url":"https://fosstodon.example/@alice"`) || !strings.Contains(out, `"url":"https://alice.example/now"`) { t.Fatalf("links not stored: %s", out) } if _, _, code := inst.ssh(t, aliceKey, "", "profile", "set", "--link", "'x|gopher://nope'"); code != 2 { t.Fatal("bad link scheme accepted") } sixth := []string{"profile", "set"} for i := 0; i < 6; i++ { sixth = append(sixth, "--link", "https://example.org/"+string(rune('a'+i))) } if _, _, code := inst.ssh(t, aliceKey, "", sixth...); code != 2 { t.Fatal("more than five links accepted") } // Owner pages render description and website link. status, body = inst.get(t, "/alice") if status != 200 || !strings.Contains(body, "tinkerer") { t.Fatalf("user page profile: %d", status) } // About renders as markdown between the header and the activity graph; // links render as chips. if !strings.Contains(body, "small tools") { t.Fatalf("about not rendered: %s", body) } if !strings.Contains(body, `href="https://fosstodon.example/@alice"`) || !strings.Contains(body, ">Mastodon<") { t.Fatalf("links not rendered: %s", body) } if strings.Index(body, "small tools") > strings.Index(body, `class="activity"`) { t.Error("about renders below the activity graph") } if strings.Index(body, `