oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs_test
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/base64"
7 "encoding/json"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/docs/internal/docs"
15)
16
17func TestNativeProofAndSessionRevocation(t *testing.T) {
18 app := members(t, "alice@example.com")
19 browser := signInCookies(t, app, "alice@example.com")
20 verifier := base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{42}, 32))
21 hash := sha256.Sum256([]byte(verifier))
22 begin := nativeRequest(app, nil, "POST", "/native/request", `{"challenge":"`+base64.RawURLEncoding.EncodeToString(hash[:])+`"}`)
23 var pending struct{ ID string }
24 if begin.Code != 200 || json.Unmarshal(begin.Body.Bytes(), &pending) != nil || pending.ID == "" {
25 t.Fatalf("native begin: %d", begin.Code)
26 }
27 exchange := `{"id":"` + pending.ID + `","verifier":"` + verifier + `"}`
28 if got := nativeRequest(app, nil, "POST", "/native/exchange", exchange); got.Code != 202 || strings.Contains(got.Body.String(), `"value"`) {
29 t.Fatalf("pending leaks credential: %d", got.Code)
30 }
31 wrong := nativeRequest(app, nil, "POST", "/native/exchange", `{"id":"`+pending.ID+`","verifier":"wrong"}`)
32 if wrong.Code == 200 {
33 t.Fatal("wrong proof accepted")
34 }
35 if got := nativeRequest(app, browser, "POST", "/native/authorize?id="+pending.ID, ""); got.Code != 200 {
36 t.Fatalf("approve: %d", got.Code)
37 }
38 done := nativeRequest(app, nil, "POST", "/native/exchange", exchange)
39 var credential struct{ Name, Value string }
40 if done.Code != 200 || json.Unmarshal(done.Body.Bytes(), &credential) != nil || credential.Value == "" {
41 t.Fatalf("exchange: %d", done.Code)
42 }
43 native := []*http.Cookie{{Name: credential.Name, Value: credential.Value}}
44 if got := nativeRequest(app, native, "GET", "/", ""); got.Code != 200 {
45 t.Fatalf("native session rejected: %d", got.Code)
46 }
47 nativeRequest(app, native, "POST", "/signout", "")
48 if got := nativeRequest(app, native, "GET", "/", ""); got.Code != 303 {
49 t.Fatalf("revoked native session accepted: %d", got.Code)
50 }
51 if got := nativeRequest(app, browser, "GET", "/", ""); got.Code != 200 {
52 t.Fatalf("native signout incorrectly revoked browser: %d", got.Code)
53 }
54 request := httptest.NewRequest("POST", "/native/request", strings.NewReader(`{"challenge":"`+base64.RawURLEncoding.EncodeToString(hash[:])+`"}`))
55 request.Header.Set("Origin", "https://foreign.invalid")
56 request.Header.Set("Sec-Fetch-Site", "cross-site")
57 recorder := httptest.NewRecorder()
58 app.Handler().ServeHTTP(recorder, request)
59 if recorder.Code != 403 {
60 t.Fatalf("cross-origin native begin: %d", recorder.Code)
61 }
62}
63
64func nativeRequest(app *docs.App, cookies []*http.Cookie, method, path, form string) *httptest.ResponseRecorder {
65 r := httptest.NewRequest(method, path, strings.NewReader(form))
66 r.Header.Set("Accept", "application/json")
67 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
68 if strings.HasPrefix(form, "{") {
69 r.Header.Set("Content-Type", "application/json")
70 }
71 r.Header.Set("Sec-Fetch-Site", "same-origin")
72 for _, cookie := range cookies {
73 r.AddCookie(cookie)
74 }
75 w := httptest.NewRecorder()
76 app.Handler().ServeHTTP(w, r)
77 return w
78}
79
80func TestNativeMetadataPreservesPermissions(t *testing.T) {
81 app := members(t, "alice@example.com,bob@example.com")
82 alice := signInCookies(t, app, "alice@example.com")
83 bob := signInCookies(t, app, "bob@example.com")
84 created := nativeRequest(app, alice, "POST", "/documents", "title=Native+minutes")
85 if created.Code != 201 {
86 t.Fatalf("create: %d %s", created.Code, created.Body.String())
87 }
88 var document struct{ Sqid, Title, GUID string }
89 if err := json.Unmarshal(created.Body.Bytes(), &document); err != nil || document.Sqid == "" || document.GUID == "" {
90 t.Fatalf("missing native identity: %s", created.Body.String())
91 }
92 path := "/d/" + document.Sqid
93 if got := nativeRequest(app, bob, "GET", path, ""); got.Code != 404 {
94 t.Fatalf("foreign document: %d", got.Code)
95 }
96 grant := nativeRequest(app, alice, "POST", path+"/share", "address=bob%40example.com&role=reader")
97 if grant.Code != 200 {
98 t.Fatalf("grant: %d", grant.Code)
99 }
100 got := nativeRequest(app, bob, "GET", path, "")
101 var detail struct {
102 CanWrite bool `json:"can_write"`
103 CanShare bool `json:"can_share"`
104 GUID string `json:"guid"`
105 }
106 if got.Code != 200 || json.Unmarshal(got.Body.Bytes(), &detail) != nil || detail.GUID == "" || detail.CanWrite || detail.CanShare {
107 t.Fatalf("reader metadata: %d %s", got.Code, got.Body.String())
108 }
109 if got.Header().Get("Cache-Control") != "no-store" {
110 t.Fatal("native content is cacheable")
111 }
112 if got := nativeRequest(app, bob, "GET", path+"/share", ""); got.Code != 404 {
113 t.Fatalf("reader sees sharing: %d", got.Code)
114 }
115 listing := nativeRequest(app, bob, "GET", "/?q=Native", "")
116 if listing.Code != 200 || !strings.Contains(listing.Body.String(), document.Sqid) {
117 t.Fatalf("shared search missing: %s", listing.Body.String())
118 }
119 revoked := nativeRequest(app, alice, "POST", path+"/share/revoke", "address=bob%40example.com")
120 if revoked.Code != 200 {
121 t.Fatalf("revoke: %d", revoked.Code)
122 }
123 if got := nativeRequest(app, bob, "GET", path, ""); got.Code != 404 {
124 t.Fatalf("revoked reader: %d", got.Code)
125 }
126 listing = nativeRequest(app, bob, "GET", "/", "")
127 if strings.Contains(listing.Body.String(), document.Sqid) {
128 t.Fatal("revoked document remains in native list")
129 }
130}
131
132func TestNativePublicLinkShownOnceAndAnonymousRefused(t *testing.T) {
133 app := members(t, "alice@example.com")
134 alice := signInCookies(t, app, "alice@example.com")
135 sqid := createDoc(t, app, alice, "Published native")
136 path := "/d/" + sqid + "/share"
137 pub := nativeRequest(app, alice, "POST", path+"/link", "on=1")
138 var result struct {
139 Published bool `json:"published"`
140 URL string `json:"public_url"`
141 }
142 if pub.Code != 200 || json.Unmarshal(pub.Body.Bytes(), &result) != nil || !result.Published || result.URL == "" {
143 t.Fatalf("publish: %d %s", pub.Code, pub.Body.String())
144 }
145 if pub.Header().Get("Cache-Control") != "no-store" {
146 t.Fatal("public token response cacheable")
147 }
148 url, err := url.Parse(result.URL)
149 if err != nil || !strings.HasPrefix(url.Path, "/p/") {
150 t.Fatal("invalid public link")
151 }
152 next := nativeRequest(app, alice, "GET", path, "")
153 if next.Code != 200 || strings.Contains(next.Body.String(), result.URL) {
154 t.Fatal("public token returned twice")
155 }
156 if got := nativeRequest(app, nil, "GET", "/", ""); got.Code != 303 {
157 t.Fatalf("anonymous native listing: %d", got.Code)
158 }
159 if got := nativeRequest(app, nil, "POST", "/documents", "title=No"); got.Code != 403 {
160 t.Fatalf("anonymous native creation: %d", got.Code)
161 }
162}
163
164func TestNativeDraftCreationRetryAndRename(t *testing.T) {
165 app := members(t, "alice@example.com,bob@example.com")
166 alice := signInCookies(t, app, "alice@example.com")
167 bob := signInCookies(t, app, "bob@example.com")
168 const form = "creation_token=dd1f5245-2d96-4f11-af6b-350de8c32896"
169 // Concurrent/lost-response retries must all resolve the same owner-scoped token.
170 results := make(chan *httptest.ResponseRecorder, 8)
171 for i := 0; i < 8; i++ {
172 go func() { results <- nativeRequest(app, alice, "POST", "/documents", form) }()
173 }
174 var sqid string
175 for i := 0; i < 8; i++ {
176 got := <-results
177 var doc struct{ Sqid string }
178 if got.Code != 201 || json.Unmarshal(got.Body.Bytes(), &doc) != nil {
179 t.Fatalf("create: %d %s", got.Code, got.Body.String())
180 }
181 if sqid != "" && sqid != doc.Sqid {
182 t.Fatal("retry created another document")
183 }
184 sqid = doc.Sqid
185 }
186 path := "/d/" + sqid
187 if got := nativeRequest(app, alice, "POST", path+"/title", "title=Renamed"); got.Code != 204 {
188 t.Fatalf("rename: %d %s", got.Code, got.Body.String())
189 }
190 var replay struct{ Sqid, Title string }
191 got := nativeRequest(app, alice, "POST", "/documents", form+"&title=Old")
192 if json.Unmarshal(got.Body.Bytes(), &replay) != nil || replay.Sqid != sqid || replay.Title != "Renamed" {
193 t.Fatalf("replay overwrote title: %s", got.Body.String())
194 }
195 if got := nativeRequest(app, bob, "POST", path+"/title", "title=Stolen"); got.Code != 404 {
196 t.Fatalf("stranger rename: %d", got.Code)
197 }
198 nativeRequest(app, alice, "POST", path+"/share", "address=bob%40example.com&role=reader")
199 if got := nativeRequest(app, bob, "POST", path+"/title", "title=Reader"); got.Code != 404 {
200 t.Fatalf("reader rename: %d", got.Code)
201 }
202 nativeRequest(app, alice, "POST", path+"/share", "address=bob%40example.com&role=writer")
203 if got := nativeRequest(app, bob, "POST", path+"/title", "title=Writer"); got.Code != 204 {
204 t.Fatalf("writer rename: %d", got.Code)
205 }
206 if got := nativeRequest(app, alice, "POST", path+"/title", "title="+strings.Repeat("a", 1025)); got.Code != 400 {
207 t.Fatalf("oversized title: %d", got.Code)
208 }
209 other := nativeRequest(app, bob, "POST", "/documents", form)
210 if json.Unmarshal(other.Body.Bytes(), &replay) != nil || replay.Sqid == sqid {
211 t.Fatal("creation token escaped owner scope")
212 }
213}
214