| // Copyright 2022 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package loginsessions |
| |
| import ( |
| "context" |
| "encoding/base64" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
| "net/http/cookiejar" |
| "net/http/httptest" |
| "net/url" |
| "strings" |
| "testing" |
| "time" |
| |
| "google.golang.org/grpc/codes" |
| "google.golang.org/grpc/metadata" |
| "google.golang.org/protobuf/types/known/durationpb" |
| "google.golang.org/protobuf/types/known/timestamppb" |
| |
| "go.chromium.org/luci/auth/loginsessionspb" |
| "go.chromium.org/luci/common/clock/testclock" |
| "go.chromium.org/luci/common/logging/gologger" |
| "go.chromium.org/luci/common/testing/ftt" |
| "go.chromium.org/luci/common/testing/truth/assert" |
| "go.chromium.org/luci/common/testing/truth/should" |
| "go.chromium.org/luci/grpc/grpcutil/testing/grpccode" |
| |
| "go.chromium.org/luci/server/loginsessions/internal" |
| "go.chromium.org/luci/server/loginsessions/internal/statepb" |
| "go.chromium.org/luci/server/router" |
| "go.chromium.org/luci/server/secrets" |
| ) |
| |
| const mockedAuthorizationEndpoint = "http://localhost/authorization" |
| |
| func TestModule(t *testing.T) { |
| t.Parallel() |
| |
| ftt.Run("With module", t, func(t *ftt.Test) { |
| var now = testclock.TestRecentTimeUTC.Round(time.Millisecond) |
| |
| timestampFromNow := func(d time.Duration) *timestamppb.Timestamp { |
| return timestamppb.New(now.Add(d)) |
| } |
| |
| ctx, tc := testclock.UseTime(context.Background(), now) |
| ctx = gologger.StdConfig.Use(ctx) |
| ctx = secrets.GeneratePrimaryTinkAEADForTest(ctx) |
| |
| opts := &ModuleOptions{ |
| RootURL: "", // set below after we get httptest server |
| } |
| mod := &loginSessionsModule{ |
| opts: opts, |
| srv: &loginSessionsServer{ |
| opts: opts, |
| store: &internal.MemorySessionStore{}, |
| provider: func(_ context.Context, id string) (*internal.OAuthClient, error) { |
| if id == "allowed-client-id" { |
| return &internal.OAuthClient{ |
| AuthorizationEndpoint: mockedAuthorizationEndpoint, |
| }, nil |
| } |
| return nil, nil |
| }, |
| }, |
| insecureCookie: true, |
| } |
| |
| handler := router.New() |
| handler.Use(router.MiddlewareChain{ |
| func(rc *router.Context, next router.Handler) { |
| rc.Request = rc.Request.WithContext(ctx) |
| next(rc) |
| }, |
| }) |
| mod.installRoutes(handler) |
| srv := httptest.NewServer(handler) |
| defer srv.Close() |
| |
| opts.RootURL = srv.URL |
| |
| jar, err := cookiejar.New(nil) |
| assert.Loosely(t, err, should.BeNil) |
| web := &http.Client{Jar: jar} |
| |
| // Union of all template args ever passed to templates. |
| type templateArgs struct { |
| Template string |
| Session *statepb.LoginSession |
| OAuthClient *internal.OAuthClient |
| OAuthState string |
| OAuthRedirectParams map[string]string |
| BadCode bool |
| Error string |
| } |
| |
| parseWebResponse := func(resp *http.Response, err error) templateArgs { |
| assert.Loosely(t, err, should.BeNil) |
| defer resp.Body.Close() |
| body, err := io.ReadAll(resp.Body) |
| assert.Loosely(t, err, should.BeNil) |
| var args templateArgs |
| assert.Loosely(t, json.Unmarshal(body, &args), should.BeNil) |
| return args |
| } |
| |
| webGET := func(url string) templateArgs { |
| return parseWebResponse(web.Get(url)) |
| } |
| |
| webPOST := func(url string, vals url.Values) templateArgs { |
| return parseWebResponse(web.PostForm(url, vals)) |
| } |
| |
| createSessionReq := func() *loginsessionspb.CreateLoginSessionRequest { |
| return &loginsessionspb.CreateLoginSessionRequest{ |
| OauthClientId: "allowed-client-id", |
| OauthScopes: []string{"scope-0", "scope-1"}, |
| OauthS256CodeChallenge: "code-challenge", |
| ExecutableName: "executable", |
| ClientHostname: "hostname", |
| } |
| } |
| |
| t.Run("CreateLoginSession + GetLoginSession", func(t *ftt.Test) { |
| session, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, session, should.Match(&loginsessionspb.LoginSession{ |
| Id: session.Id, |
| Password: session.Password, |
| State: loginsessionspb.LoginSession_PENDING, |
| Created: timestampFromNow(0), |
| Expiry: timestampFromNow(sessionExpiry), |
| LoginFlowUrl: fmt.Sprintf("%s/cli/login/%s", srv.URL, session.Id), |
| PollInterval: durationpb.New(time.Second), |
| ConfirmationCode: session.ConfirmationCode, |
| ConfirmationCodeExpiry: durationpb.New(confirmationCodeExpiryMax), |
| ConfirmationCodeRefresh: durationpb.New(confirmationCodeExpiryRefresh), |
| })) |
| |
| pwd := session.Password |
| session.Password = nil |
| |
| got, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: pwd, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, got, should.Match(session)) |
| |
| // Later the confirmation code gets stale and a new one is generated. |
| tc.Set(now.Add(confirmationCodeExpiryRefresh + time.Second)) |
| got1, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: pwd, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, got1.ConfirmationCode, should.NotEqual(session.ConfirmationCode)) |
| |
| // Have two codes in the store right now. |
| stored, err := mod.srv.store.Get(ctx, session.Id) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, stored.ConfirmationCodes, should.HaveLength(2)) |
| |
| // Later the expired code is kicked out of the storage. |
| tc.Set(now.Add(confirmationCodeExpiryMax + time.Second)) |
| got2, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: pwd, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, got2.ConfirmationCode, should.Equal(got1.ConfirmationCode)) |
| |
| // Have only one code now. |
| stored, err = mod.srv.store.Get(ctx, session.Id) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, stored.ConfirmationCodes, should.HaveLength(1)) |
| |
| // Later the session itself expires. |
| tc.Set(now.Add(sessionExpiry + time.Second)) |
| exp, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: pwd, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, exp, should.Match(&loginsessionspb.LoginSession{ |
| Id: session.Id, |
| State: loginsessionspb.LoginSession_EXPIRED, |
| Created: timestampFromNow(0), |
| Expiry: timestampFromNow(sessionExpiry), |
| Completed: timestampFromNow(sessionExpiry + time.Second), |
| LoginFlowUrl: fmt.Sprintf("%s/cli/login/%s", srv.URL, session.Id), |
| })) |
| |
| // And this is the final stage. |
| tc.Set(now.Add(sessionExpiry + time.Hour)) |
| exp2, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: pwd, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, exp2, should.Match(exp)) |
| }) |
| |
| t.Run("CreateLoginSession validation", func(t *ftt.Test) { |
| t.Run("Browser headers", func(t *ftt.Test) { |
| ctx := metadata.NewIncomingContext(ctx, metadata.Pairs("sec-fetch-site", "none")) |
| _, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.PermissionDenied)) |
| }) |
| t.Run("Missing OAuth client ID", func(t *ftt.Test) { |
| req := createSessionReq() |
| req.OauthClientId = "" |
| _, err := mod.srv.CreateLoginSession(ctx, req) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.InvalidArgument)) |
| }) |
| t.Run("Missing OAuth scopes", func(t *ftt.Test) { |
| req := createSessionReq() |
| req.OauthScopes = nil |
| _, err := mod.srv.CreateLoginSession(ctx, req) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.InvalidArgument)) |
| }) |
| t.Run("Missing OAuth challenge", func(t *ftt.Test) { |
| req := createSessionReq() |
| req.OauthS256CodeChallenge = "" |
| _, err := mod.srv.CreateLoginSession(ctx, req) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.InvalidArgument)) |
| }) |
| t.Run("Unknown OAuth client", func(t *ftt.Test) { |
| req := createSessionReq() |
| req.OauthClientId = "unknown" |
| _, err := mod.srv.CreateLoginSession(ctx, req) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.PermissionDenied)) |
| }) |
| }) |
| |
| t.Run("GetLoginSession validation", func(t *ftt.Test) { |
| session, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, should.BeNil) |
| |
| t.Run("Browser headers", func(t *ftt.Test) { |
| ctx := metadata.NewIncomingContext(ctx, metadata.Pairs("sec-fetch-site", "none")) |
| _, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.PermissionDenied)) |
| }) |
| t.Run("Missing ID", func(t *ftt.Test) { |
| _, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.InvalidArgument)) |
| }) |
| t.Run("Missing password", func(t *ftt.Test) { |
| _, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| }) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.InvalidArgument)) |
| }) |
| t.Run("Missing session", func(t *ftt.Test) { |
| _, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: "missing", |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.NotFound)) |
| }) |
| t.Run("Wrong password", func(t *ftt.Test) { |
| _, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: []byte("wrong"), |
| }) |
| assert.Loosely(t, err, grpccode.ShouldBe(codes.NotFound)) |
| }) |
| }) |
| |
| t.Run("Full successful flow", func(t *ftt.Test) { |
| // The CLI tool starts a new login session. |
| sessionReq := createSessionReq() |
| session, err := mod.srv.CreateLoginSession(ctx, sessionReq) |
| assert.Loosely(t, err, should.BeNil) |
| |
| // The user opens the web page with the session and gets the cookie and |
| // the authorization endpoint redirect parameters. |
| tmpl := webGET(session.LoginFlowUrl) |
| assert.Loosely(t, tmpl.Error, should.BeEmpty) |
| assert.Loosely(t, tmpl.Template, should.Equal("pages/start.html")) |
| assert.Loosely(t, tmpl.OAuthState, should.NotBeEmpty) |
| assert.Loosely(t, tmpl.OAuthRedirectParams, should.Match(map[string]string{ |
| "access_type": "offline", |
| "client_id": sessionReq.OauthClientId, |
| "code_challenge": sessionReq.OauthS256CodeChallenge, |
| "code_challenge_method": "S256", |
| "nonce": session.Id, |
| "prompt": "consent", |
| "redirect_uri": srv.URL + "/cli/confirm", |
| "response_type": "code", |
| "scope": strings.Join(sessionReq.OauthScopes, " "), |
| "state": tmpl.OAuthState, |
| })) |
| |
| // The user goes through the login flow and ends up back with a code. |
| // This renders a page asking for the confirmation code. |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "code": {"authorization-code"}, |
| "state": {tmpl.OAuthState}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.BeEmpty) |
| assert.Loosely(t, tmpl.Template, should.Equal("pages/confirm.html")) |
| assert.Loosely(t, tmpl.OAuthState, should.NotBeEmpty) |
| |
| // A correct confirmation code is entered and accepted. |
| tmpl = webPOST(confirmURL, url.Values{ |
| "confirmation_code": {session.ConfirmationCode}, |
| }) |
| assert.Loosely(t, tmpl.Error, should.BeEmpty) |
| assert.Loosely(t, tmpl.Template, should.Equal("pages/success.html")) |
| |
| // The session is completed and the code is returned to the CLI. |
| session, err = mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, session, should.Match(&loginsessionspb.LoginSession{ |
| Id: session.Id, |
| State: loginsessionspb.LoginSession_SUCCEEDED, |
| Created: timestampFromNow(0), |
| Expiry: timestampFromNow(sessionExpiry), |
| Completed: timestampFromNow(0), |
| LoginFlowUrl: fmt.Sprintf("%s/cli/login/%s", srv.URL, session.Id), |
| OauthAuthorizationCode: "authorization-code", |
| OauthRedirectUrl: srv.URL + "/cli/confirm", |
| })) |
| |
| // Visiting the session page again shows it is gone. |
| tmpl = webGET(session.LoginFlowUrl) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("No such login session")) |
| }) |
| |
| t.Run("Session page errors", func(t *ftt.Test) { |
| session, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, should.BeNil) |
| |
| t.Run("Wrong session ID", func(t *ftt.Test) { |
| // Opening a non-existent session page is an error. |
| tmpl := webGET(session.LoginFlowUrl + "extra") |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("No such login session")) |
| }) |
| |
| t.Run("Expired session", func(t *ftt.Test) { |
| // Opening an old session is an error. |
| tc.Add(sessionExpiry + time.Second) |
| tmpl := webGET(session.LoginFlowUrl) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("No such login session")) |
| }) |
| }) |
| |
| t.Run("Redirect page errors", func(t *ftt.Test) { |
| // Create the session and get the session cookie. |
| session, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, should.BeNil) |
| tmpl := webGET(session.LoginFlowUrl) |
| assert.Loosely(t, tmpl.OAuthState, should.NotBeEmpty) |
| |
| checkSessionState := func(state loginsessionspb.LoginSession_State, msg string) { |
| session, err := mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, session.State, should.Equal(state)) |
| assert.Loosely(t, session.OauthError, should.Equal(msg)) |
| } |
| |
| t.Run("OK", func(t *ftt.Test) { |
| // Just double check the test setup is correct. |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| webPOST(confirmURL, url.Values{ |
| "confirmation_code": {session.ConfirmationCode}, |
| }) |
| checkSessionState(loginsessionspb.LoginSession_SUCCEEDED, "") |
| }) |
| |
| t.Run("No OAuth code or error", func(t *ftt.Test) { |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("The authorization provider returned error code: unknown")) |
| checkSessionState(loginsessionspb.LoginSession_FAILED, "unknown") |
| }) |
| |
| t.Run("OAuth error", func(t *ftt.Test) { |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "error": {"boom"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("The authorization provider returned error code: boom")) |
| checkSessionState(loginsessionspb.LoginSession_FAILED, "boom") |
| }) |
| |
| t.Run("No state", func(t *ftt.Test) { |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "error": {"boom"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("The authorization provider returned error code: boom")) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Bad state", func(t *ftt.Test) { |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState[:20]}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("Internal server error")) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Expired session", func(t *ftt.Test) { |
| tc.Add(sessionExpiry + time.Second) |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("finished or expired")) |
| checkSessionState(loginsessionspb.LoginSession_EXPIRED, "") |
| }) |
| |
| t.Run("Missing login cookie", func(t *ftt.Test) { |
| emptyJar, err := cookiejar.New(nil) |
| assert.Loosely(t, err, should.BeNil) |
| web.Jar = emptyJar |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("finished or expired")) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Wrong login cookie", func(t *ftt.Test) { |
| u, err := url.Parse(srv.URL + "/cli/session") |
| assert.Loosely(t, err, should.BeNil) |
| jar.SetCookies(u, []*http.Cookie{ |
| { |
| Name: mod.loginCookieName(session.Id), |
| Value: base64.RawURLEncoding.EncodeToString([]byte("wrong")), |
| Path: "/cli/", |
| MaxAge: 100000, |
| HttpOnly: true, |
| }, |
| }) |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| tmpl = webGET(confirmURL) |
| assert.Loosely(t, tmpl.Error, should.ContainSubstring("finished or expired")) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Wrong confirmation code", func(t *ftt.Test) { |
| confirmURL := srv.URL + "/cli/confirm?" + (url.Values{ |
| "state": {tmpl.OAuthState}, |
| "code": {"authorization-code"}, |
| }).Encode() |
| |
| t.Run("Empty", func(t *ftt.Test) { |
| tmpl = webPOST(confirmURL, url.Values{ |
| "confirmation_code": {""}, |
| }) |
| assert.Loosely(t, tmpl.BadCode, should.BeTrue) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Wrong", func(t *ftt.Test) { |
| tmpl = webPOST(confirmURL, url.Values{ |
| "confirmation_code": {"wrong"}, |
| }) |
| assert.Loosely(t, tmpl.BadCode, should.BeTrue) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| |
| t.Run("Stale, but still valid", func(t *ftt.Test) { |
| tc.Add(confirmationCodeExpiryRefresh + time.Second) |
| webPOST(confirmURL, url.Values{ |
| "confirmation_code": {session.ConfirmationCode}, |
| }) |
| checkSessionState(loginsessionspb.LoginSession_SUCCEEDED, "") |
| }) |
| |
| t.Run("Expired", func(t *ftt.Test) { |
| tc.Add(confirmationCodeExpiryMax + time.Second) |
| tmpl = webPOST(confirmURL, url.Values{ |
| "confirmation_code": {session.ConfirmationCode}, |
| }) |
| assert.Loosely(t, tmpl.BadCode, should.BeTrue) |
| checkSessionState(loginsessionspb.LoginSession_PENDING, "") |
| }) |
| }) |
| }) |
| |
| t.Run("Session cancellation", func(t *ftt.Test) { |
| // Create the session and get the session cookie. |
| session, err := mod.srv.CreateLoginSession(ctx, createSessionReq()) |
| assert.Loosely(t, err, should.BeNil) |
| tmpl := webGET(session.LoginFlowUrl) |
| assert.Loosely(t, tmpl.OAuthState, should.NotBeEmpty) |
| |
| // Cancel it. |
| tmpl = webPOST(srv.URL+"/cli/cancel", url.Values{ |
| "state": {tmpl.OAuthState}, |
| }) |
| assert.Loosely(t, tmpl.Template, should.Equal("pages/canceled.html")) |
| |
| // Verify it is indeed canceled. |
| session, err = mod.srv.GetLoginSession(ctx, &loginsessionspb.GetLoginSessionRequest{ |
| LoginSessionId: session.Id, |
| LoginSessionPassword: session.Password, |
| }) |
| assert.Loosely(t, err, should.BeNil) |
| assert.Loosely(t, session.State, should.Equal(loginsessionspb.LoginSession_CANCELED)) |
| }) |
| }) |
| } |