Go · 6068 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package entitlements_test
4
5 import (
6 "context"
7 "log/slog"
8 "net/http/httptest"
9 "testing"
10
11 "github.com/tenseleyFlow/shithub/internal/billing"
12 "github.com/tenseleyFlow/shithub/internal/entitlements"
13 )
14
15 func TestAppliesToOrgOnlyFeatures(t *testing.T) {
16 t.Parallel()
17 orgOnly := []entitlements.Feature{
18 entitlements.FeatureSecretTeams,
19 entitlements.FeatureActionsOrgSecrets,
20 entitlements.FeatureActionsOrgVariables,
21 entitlements.FeaturePrivateCollaboration,
22 entitlements.FeatureStorageQuota,
23 entitlements.FeatureActionsMinutesQuota,
24 }
25 for _, f := range orgOnly {
26 if !entitlements.FeatureAppliesToKind(f, billing.SubjectKindOrg) {
27 t.Errorf("%s should apply to org kind", f)
28 }
29 if entitlements.FeatureAppliesToKind(f, billing.SubjectKindUser) {
30 t.Errorf("%s should NOT apply to user kind", f)
31 }
32 }
33 }
34
35 func TestAppliesToCrossKindFeatures(t *testing.T) {
36 t.Parallel()
37 crossKind := []entitlements.Feature{
38 entitlements.FeatureAdvancedBranchProtection,
39 entitlements.FeatureRequiredReviewers,
40 entitlements.FeatureCodeOwnersReview,
41 }
42 for _, f := range crossKind {
43 if !entitlements.FeatureAppliesToKind(f, billing.SubjectKindOrg) {
44 t.Errorf("%s should apply to org kind", f)
45 }
46 if !entitlements.FeatureAppliesToKind(f, billing.SubjectKindUser) {
47 t.Errorf("%s should apply to user kind", f)
48 }
49 }
50 }
51
52 // TestAppliesToUserOnlyFeatures locks the PRO07-introduced user-only
53 // features. FeatureProfilePinsBeyondFree must not be queryable for
54 // org kind — orgs share the visible Free cap (PRO01 ratification).
55 func TestAppliesToUserOnlyFeatures(t *testing.T) {
56 t.Parallel()
57 userOnly := []entitlements.Feature{
58 entitlements.FeatureProfilePinsBeyondFree,
59 }
60 for _, f := range userOnly {
61 if !entitlements.FeatureAppliesToKind(f, billing.SubjectKindUser) {
62 t.Errorf("%s should apply to user kind", f)
63 }
64 if entitlements.FeatureAppliesToKind(f, billing.SubjectKindOrg) {
65 t.Errorf("%s should NOT apply to org kind", f)
66 }
67 }
68 }
69
70 func TestDeprecatedAliasesPointAtCanonical(t *testing.T) {
71 t.Parallel()
72 // SP-era constants are aliases for the renamed ones. Code that
73 // uses FeatureOrgSecretTeams must read the same value as
74 // FeatureSecretTeams.
75 cases := []struct {
76 legacy, canonical entitlements.Feature
77 }{
78 {entitlements.FeatureOrgSecretTeams, entitlements.FeatureSecretTeams},
79 {entitlements.FeatureOrgAdvancedBranchProtection, entitlements.FeatureAdvancedBranchProtection},
80 {entitlements.FeatureOrgRequiredReviewers, entitlements.FeatureRequiredReviewers},
81 {entitlements.FeatureOrgActionsSecrets, entitlements.FeatureActionsOrgSecrets},
82 {entitlements.FeatureOrgActionsVariables, entitlements.FeatureActionsOrgVariables},
83 {entitlements.FeatureOrgPrivateCollaboration, entitlements.FeaturePrivateCollaboration},
84 {entitlements.FeatureOrgStorageQuota, entitlements.FeatureStorageQuota},
85 {entitlements.FeatureOrgActionsMinutesQuota, entitlements.FeatureActionsMinutesQuota},
86 }
87 for _, tc := range cases {
88 if tc.legacy != tc.canonical {
89 t.Errorf("alias %q != canonical %q", tc.legacy, tc.canonical)
90 }
91 }
92 }
93
94 // TestRequirePrincipalFeatureUnknownKind verifies the short-circuit
95 // when the feature doesn't apply to the principal's kind. Org-only
96 // features on personal repos must allow without logging — they
97 // represent "this resource type isn't gateable here."
98 func TestRequirePrincipalFeatureUnknownKind(t *testing.T) {
99 t.Parallel()
100 w := httptest.NewRecorder()
101 ok, res := entitlements.RequirePrincipalFeature(
102 context.Background(),
103 w,
104 nil, // pool unused on short-circuit path
105 billing.PrincipalForUser(42),
106 entitlements.FeatureSecretTeams, // org-only
107 entitlements.NewRequireOpts("label", ""),
108 )
109 if !ok {
110 t.Fatalf("inapplicable feature should short-circuit ok=true")
111 }
112 if !res.UnknownKindForFeature {
113 t.Errorf("UnknownKindForFeature should be true")
114 }
115 if res.WouldDenyButLog {
116 t.Errorf("WouldDenyButLog should be false for inapplicable feature")
117 }
118 }
119
120 // TestPrincipalUpgradeBannerForUser verifies the kind-aware banner
121 // copy: user kind says "Pro" and points at /settings/billing; org
122 // kind says "Team" and points at the org settings page.
123 func TestPrincipalUpgradeBannerForUser(t *testing.T) {
124 t.Parallel()
125 decision := entitlements.Decision{Reason: entitlements.ReasonUpgradeRequired}
126 user := billing.PrincipalForUser(7)
127 banner := decision.PrincipalUpgradeBanner("Required reviewers", user, "")
128 if banner.ActionHref != "/settings/billing" {
129 t.Errorf("user banner href: got %q, want /settings/billing", banner.ActionHref)
130 }
131 if banner.Message == "" || !contains(banner.Message, "Pro") {
132 t.Errorf("user banner message should mention Pro: %q", banner.Message)
133 }
134 }
135
136 func TestPrincipalUpgradeBannerForOrg(t *testing.T) {
137 t.Parallel()
138 decision := entitlements.Decision{Reason: entitlements.ReasonUpgradeRequired}
139 org := billing.PrincipalForOrg(7)
140 banner := decision.PrincipalUpgradeBanner("Required reviewers", org, "acme")
141 if banner.ActionHref != "/organizations/acme/settings/billing" {
142 t.Errorf("org banner href: got %q", banner.ActionHref)
143 }
144 if !contains(banner.Message, "Team") {
145 t.Errorf("org banner message should mention Team: %q", banner.Message)
146 }
147 }
148
149 // TestRequirePrincipalFeatureWithLogger smoke-tests that the helper
150 // runs without panic when given a logger and a nil pool — the
151 // short-circuit path takes effect for inapplicable features.
152 func TestRequirePrincipalFeatureWithLogger(t *testing.T) {
153 t.Parallel()
154 logger := slog.New(slog.DiscardHandler)
155 opts := entitlements.NewRequireOpts("label", "acme")
156 opts.Logger = logger
157 w := httptest.NewRecorder()
158 ok, _ := entitlements.RequirePrincipalFeature(
159 context.Background(),
160 w,
161 nil,
162 billing.PrincipalForUser(1),
163 entitlements.FeatureActionsOrgSecrets, // org-only, inapplicable to user
164 opts,
165 )
166 if !ok {
167 t.Errorf("inapplicable feature: expected ok=true")
168 }
169 }
170
171 func contains(s, sub string) bool {
172 for i := 0; i+len(sub) <= len(s); i++ {
173 if s[i:i+len(sub)] == sub {
174 return true
175 }
176 }
177 return false
178 }
179