package sheets import ( "amadan.net/oficina/home/connect" "amadan.net/rastrillo/rastrillo" "context" "html" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestHomeLocalPluginRoundTripAndSignout(t *testing.T) { f := newHomeFixture(t) if w := f.request(t, "GET", "/account/home", nil, false); w.Code != 303 { t.Fatal(w.Code) } if w := f.request(t, "POST", "/signin", url.Values{"address": {signedInAddress}, "force": {"1"}}, false); w.Code != 303 { t.Fatal(w.Code) } u, err := url.Parse(f.app.testMailer(t).lastLink(t)) if err != nil { t.Fatal(err) } // Opening the link draws the confirm door under LocalGuard and // spends nothing; its Sign in button POSTs the token back, and // that POST is the local callback. open := f.request(t, "GET", u.RequestURI(), nil, false) m := confirmForm.FindStringSubmatch(open.Body.String()) if open.Code != 200 || m == nil || !strings.Contains(open.Body.String(), "estilo-door") { t.Fatalf("Home-local link: %d, want the confirm door\n%s", open.Code, open.Body.String()) } verify := f.request(t, "POST", html.UnescapeString(m[1]), url.Values{"token": {html.UnescapeString(m[2])}}, false) if verify.Code != 303 || verify.Header().Get("Location") != connect.LocalCompletePath { t.Fatal("local callback", verify.Code, verify.Header().Get("Location")) } completed := f.request(t, "GET", connect.LocalCompletePath, nil, false) if completed.Code != 303 || completed.Header().Get("Location") != "/account/home" { t.Fatal("local completion", completed.Code, completed.Header().Get("Location")) } if page := f.request(t, "GET", "/account/home", nil, false); page.Code != 200 { t.Fatal("local account unavailable", page.Code) } var count int64 f.app.db.Model(&Member{}).Count(&count) if count != 1 { t.Fatal("local member missing", count) } f.app.db.Table("sheets").Count(&count) if count != 0 { t.Fatal("sign-in seeded content", count) } f.link(t, "round-trip") // With Home configured, the app's Sign out ends the app session and // then chains to the credential origin (home/connect 01166fc, spec // ยง9.5): a 307, so the POST survives the hop, to {Issuer}/signout. // That hop is what stops the signed-out page's "Sign in" passing // silently back through a live id.* session. out := f.request(t, "POST", "/signout", nil, false) if out.Code != http.StatusTemporaryRedirect || out.Header().Get("Location") != homeTestIssuer+"/signout" { t.Fatal("signout", out.Code, out.Header().Get("Location")) } if page := f.request(t, "GET", "/signed-out", nil, false); page.Code != 200 || page.Header().Get("Location") != "" { t.Fatal("signout restarted Home") } } func TestHomeDeletedMemberIsNeverReassigned(t *testing.T) { f := newHomeFixture(t) old := f.local(t, signedInAddress) f.link(t, "deleted-person") if err := f.app.db.Exec("DELETE FROM members WHERE id=?", old.ID).Error; err != nil { t.Fatal(err) } replacement, err := f.app.memberFor(signedInAddress) if err != nil { t.Fatal(err) } if replacement.ID <= old.ID { t.Fatal("member IDs reused") } if _, err := f.app.home.Resolve(context.Background(), "deleted-person"); err == nil { t.Fatal("deleted account resolved to replacement") } } func TestHomeFrameworkPolicyAndDuplicateCookies(t *testing.T) { f := newHomeFixture(t) f.local(t, signedInAddress) mux := http.NewServeMux() mux.Handle("/", f.app.Router()) handler, close, err := rastrillo.Handler(rastrillo.Options{Mux: mux}) if err != nil { t.Fatal(err) } defer close() r := httptest.NewRequest("GET", testOrigin+"/signin/local", nil) w := httptest.NewRecorder() handler.ServeHTTP(w, r) got := w.Header().Get("Content-Security-Policy") if !strings.Contains(got, "form-action 'self' "+homeTestIssuer) || !strings.Contains(got, "frame-ancestors 'none'") { t.Fatal("Home policy lost", got) } r = httptest.NewRequest("GET", testOrigin+"/account/home", nil) for _, c := range f.jar { r.AddCookie(c) r.AddCookie(c) } w = httptest.NewRecorder() handler.ServeHTTP(w, r) if w.Code != 400 { t.Fatal("duplicate session accepted", w.Code) } } func TestHomeDisabledRefusesRetainedOpaqueSession(t *testing.T) { f := newHomeFixture(t) f.local(t, signedInAddress) f.link(t, "retained-home") f.jar = map[string]*http.Cookie{} start := f.request(t, "GET", "/auth/start", nil, false) if w := f.exchange(t, start, "retained-home"); w.Code != 303 { t.Fatal(w.Code) } f.app.home = nil f.app.homeRoutes = nil f.app.homeSessions = nil var before, after int64 f.app.db.Model(&Member{}).Count(&before) w := f.request(t, "GET", "/sheets", nil, false) if w.Code != 403 { t.Fatal("retained Home session accepted", w.Code) } f.app.db.Model(&Member{}).Count(&after) if after != before { t.Fatal("opaque member created") } } func TestHomeStorageFailureDoesNotRevokeSession(t *testing.T) { f := newHomeFixture(t) f.local(t, signedInAddress) f.link(t, "storage-home") f.jar = map[string]*http.Cookie{} if w := f.exchange(t, f.request(t, "GET", "/auth/start", nil, false), "storage-home"); w.Code != 303 { t.Fatal(w.Code) } cookie := f.jar[f.app.homeSessions.CookieName()] if err := f.app.db.Exec("ALTER TABLE home_identity_links RENAME TO unavailable_links").Error; err != nil { t.Fatal(err) } w := f.request(t, "GET", "/sheets", nil, false) if w.Code != 503 { t.Fatal("storage response", w.Code) } if f.jar[cookie.Name] == nil || f.jar[cookie.Name].Value != cookie.Value { t.Fatal("storage failure revoked session") } if err := f.app.db.Exec("ALTER TABLE unavailable_links RENAME TO home_identity_links").Error; err != nil { t.Fatal(err) } if w := f.request(t, "GET", "/sheets", nil, false); w.Code != 200 { t.Fatal("session did not recover", w.Code) } }