e2e/quota_test.go
136 lines · 6384 bytes
1package e2e
2
3import (
4 "crypto/rand"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11)
12
13// Per-account and per-org caps on repositories and storage, the cap on
14// orgs an account creates, the admin overrides, and expiry of accounts
15// that never verified.
16func TestQuotasAndPendingExpiry(t *testing.T) {
17 t.Setenv("GITBAY_REAP_TICK", "500ms")
18 smtp := startFakeSMTP(t)
19 inst := startInstanceWith(t, fmt.Sprintf(
20 "[registration]\nmode = \"open\"\npending_expiry = \"2s\"\n[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n"+
21 "[limits]\nmax_repos_per_user = 2\nmax_bytes_per_user = 300000\n"+
22 "max_orgs_per_user = 1\nmax_repos_per_org = 1\n", smtp.addr))
23 rootKey := inst.newKey(t, "root")
24 aliceKey := inst.newKey(t, "alice")
25 inst.admin(t, "admin", "user", "create", "root", "--key", rootKey+".pub", "--admin")
26 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
27
28 // Two repositories fit; the third is refused with the numbers.
29 for _, r := range []string{"alice/one", "alice/two"} {
30 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", r); code != 0 {
31 t.Fatalf("create %s: %s", r, errOut)
32 }
33 }
34 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/three"); code != 4 || !strings.Contains(errOut, "2 of the 2 repositories") {
35 t.Fatalf("third repo: exit %d %s", code, errOut)
36 }
37 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "fork", "alice/one", "--name", "onefork"); code != 4 {
38 t.Fatal("fork slipped past the cap")
39 }
40 // One org fits; the second is refused.
41 if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "acme"); code != 0 {
42 t.Fatal("org create failed")
43 }
44 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "acme2"); code != 4 || !strings.Contains(errOut, "1 of the 1 organizations") {
45 t.Fatalf("second org: exit %d %s", code, errOut)
46 }
47 // The org has its own repository cap, which the admin raises per org.
48 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "acme/lib"); code != 0 {
49 t.Fatalf("org repo: %s", errOut)
50 }
51 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "acme/lib2"); code != 4 || !strings.Contains(errOut, "1 of the 1 repositories") {
52 t.Fatalf("second org repo: exit %d %s", code, errOut)
53 }
54 if out, _, code := inst.ssh(t, rootKey, "", "admin", "org", "limits", "acme", "--repos", "2"); code != 0 || !strings.Contains(out, "repos 1 of 2") {
55 t.Fatalf("org limits: exit %d %s", code, out)
56 }
57 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "acme/lib2"); code != 0 {
58 t.Fatalf("second org repo after raise: %s", errOut)
59 }
60 if out, _, code := inst.ssh(t, rootKey, "", "admin", "user", "limits", "alice", "--orgs", "2"); code != 0 || !strings.Contains(out, "orgs 1 of 2") {
61 t.Fatalf("user org limit: exit %d %s", code, out)
62 }
63 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "acme2"); code != 0 {
64 t.Fatalf("second org after raise: %s", errOut)
65 }
66 // The admin raises the cap for this account; the third fits.
67 if out, _, code := inst.ssh(t, rootKey, "", "admin", "user", "limits", "alice", "--repos", "3"); code != 0 || !strings.Contains(out, "repos 2 of 3") {
68 t.Fatalf("limits: exit %d %s", code, out)
69 }
70 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/three"); code != 0 {
71 t.Fatalf("third repo after raise: %s", errOut)
72 }
73 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "show", "alice", "--json"); !strings.Contains(out, `"repo_limit":3`) || !strings.Contains(out, `"byte_limit":300000`) {
74 t.Fatalf("show lacks limits:\n%s", out)
75 }
76 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "limits", "alice", "--repos", "default"); !strings.Contains(out, "of 2") {
77 t.Fatalf("limits back to default:\n%s", out)
78 }
79 // A transfer in counts as a create for the receiving owner.
80 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "transfer", "acme/lib2", "alice"); code != 4 || !strings.Contains(errOut, "3 of the 2 repositories") {
81 t.Fatalf("transfer past the cap: exit %d %s", code, errOut)
82 }
83 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "transfer", "alice/three", "acme2"); code != 0 {
84 t.Fatalf("transfer into an org with room: %s", errOut)
85 }
86
87 // Storage: a push past what the account has left is refused.
88 work := t.TempDir()
89 env := inst.gitEnv(aliceKey)
90 mustGit(t, work, env, "clone", inst.sshURL("alice/one"), "w")
91 dir := filepath.Join(work, "w")
92 os.WriteFile(filepath.Join(dir, "small.txt"), []byte("ok\n"), 0o644)
93 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
94 mustGit(t, dir, env, "add", ".")
95 mustGit(t, dir, env, "commit", "-q", "-m", "small")
96 mustGit(t, dir, env, "push", "-q", "origin", "main")
97 big := make([]byte, 400_000)
98 rand.Read(big)
99 os.WriteFile(filepath.Join(dir, "big.bin"), big, 0o644)
100 mustGit(t, dir, env, "add", ".")
101 mustGit(t, dir, env, "commit", "-q", "-m", "big")
102 if out, code := gitRun(t, dir, env, "push", "origin", "main"); code == 0 || !strings.Contains(out, "max") {
103 t.Fatalf("push past the storage cap accepted: exit %d\n%s", code, out)
104 }
105 if _, _, code := inst.ssh(t, rootKey, "", "admin", "user", "limits", "alice", "--bytes", "0"); code != 0 {
106 t.Fatal("lift byte cap failed")
107 }
108 mustGit(t, dir, env, "push", "-q", "origin", "main")
109
110 // An account that registers and never verifies is removed after
111 // pending_expiry; the name is free again.
112 newKey := inst.newKey(t, "dana")
113 if _, errOut, code := inst.ssh(t, newKey, "", "register", "--username", "dana", "--email", "dana@example.test"); code != 0 {
114 t.Fatalf("register: %s", errOut)
115 }
116 if out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "list", "--state", "pending"); !strings.HasPrefix(out, "dana\t") {
117 t.Fatalf("dana not pending:\n%s", out)
118 }
119 deadline := time.Now().Add(15 * time.Second)
120 for {
121 out, _, _ := inst.ssh(t, rootKey, "", "admin", "user", "list", "--state", "pending")
122 if strings.TrimSpace(out) == "" {
123 break
124 }
125 if time.Now().After(deadline) {
126 t.Fatalf("pending account never expired:\n%s", out)
127 }
128 time.Sleep(300 * time.Millisecond)
129 }
130 if _, _, code := inst.ssh(t, newKey, "", "whoami"); code == 0 {
131 t.Fatal("expired account still authenticates")
132 }
133 if out := inst.admin(t, "admin", "audit", "--action", "pending.expired"); !strings.Contains(out, `"user":"dana"`) {
134 t.Fatalf("expiry not audited:\n%s", out)
135 }
136}