44 "context"
55 "encoding/base64"
66 "encoding/json"
7+ "errors"
78 "strings"
89 "testing"
910
@@ -129,14 +130,13 @@ func TestHandleLambdaHTTPEvent_StaticPath(t *testing.T) {
129130//
130131// This drives the real failing scenario end-to-end: a base64-encoded,
131132// form-urlencoded POST body hitting POST /api/purchases/revoke/{execID}.
132- // With no session, revokeViaEmailToken (internal/api/handler_purchases.go)
133- // 401s with "sign in or use the revocation link..." when the token fails to
134- // resolve, or 401s with the DIFFERENT message "sign in with the account's
135- // contact email..." from authorizeApprovalAction once the token resolves and
136- // the (session-only) actor lookup fails. Only the second message is reachable
137- // once the token has actually been parsed out of the decoded body, so
138- // asserting it proves the fix; asserting the pre-fix code fails this test
139- // caught the bug (verified manually before landing the fix).
133+ // With a session that lacks cancel permission, revokeViaEmailToken answers 403
134+ // "permission denied" when the token fails to resolve, or falls through to the
135+ // token branch and answers the DIFFERENT 403 "no per-account contact email"
136+ // from authorizeApprovalAction once the token has been parsed out of the
137+ // decoded body. Only the second message is reachable when the token resolves,
138+ // so asserting it proves the fix. (A request with no session never reaches
139+ // either: it gets 401 before the lookup, issue #435.)
140140func TestHandleLambdaHTTPEvent_DecodesBase64FormBody (t * testing.T ) {
141141 execID := "11111111-1111-1111-1111-111111111111"
142142 exec := & config.PurchaseExecution {
@@ -146,15 +146,19 @@ func TestHandleLambdaHTTPEvent_DecodesBase64FormBody(t *testing.T) {
146146
147147 mockStore := new (mocks.MockConfigStore )
148148 mockStore .On ("GetExecutionByID" , mock .Anything , execID ).Return (exec , nil )
149+ mockStore .On ("GetGlobalConfig" , mock .Anything ).Return (& config.GlobalConfig {}, nil )
149150
150151 app := & Application {
151- API : api .NewHandler (api.HandlerConfig {ConfigStore : mockStore }),
152+ API : api .NewHandler (api.HandlerConfig {ConfigStore : mockStore , AuthService : sessionOnlyAuth {} }),
152153 }
153154
154155 encodedBody := base64 .StdEncoding .EncodeToString ([]byte ("token=body-token" ))
155156 request := events.LambdaFunctionURLRequest {
156157 RawPath : "/api/purchases/revoke/" + execID ,
157- Headers : map [string ]string {"content-type" : "application/x-www-form-urlencoded" },
158+ Headers : map [string ]string {
159+ "content-type" : "application/x-www-form-urlencoded" ,
160+ "authorization" : "Bearer sess-tok" ,
161+ },
158162 RequestContext : events.LambdaFunctionURLRequestContext {
159163 HTTP : events.LambdaFunctionURLRequestContextHTTPDescription {
160164 Method : "POST" ,
@@ -170,9 +174,9 @@ func TestHandleLambdaHTTPEvent_DecodesBase64FormBody(t *testing.T) {
170174 ctx := testutil .TestContext (t )
171175 resp , err := app .handleLambdaHTTPEvent (ctx , rawEvent )
172176 testutil .AssertNoError (t , err )
173- testutil .AssertEqual (t , 401 , resp .StatusCode )
174- testutil .AssertContains (t , resp .Body , "sign in with the account's contact email" )
175- testutil .AssertTrue (t , ! strings .Contains (resp .Body , "revocation link from the notification email " ),
177+ testutil .AssertEqual (t , 403 , resp .StatusCode )
178+ testutil .AssertContains (t , resp .Body , "no per- account contact email" )
179+ testutil .AssertTrue (t , ! strings .Contains (resp .Body , "requires cancel-any " ),
176180 "token must have been resolved from the decoded body, not left empty" )
177181
178182 mockStore .AssertExpectations (t )
@@ -191,3 +195,22 @@ func TestHandleLambdaEvent_UnknownEventRouteToScheduled(t *testing.T) {
191195 // Unknown action → error from ParseScheduledEvent
192196 testutil .AssertError (t , err )
193197}
198+
199+ // sessionOnlyAuth accepts the bearer "sess-tok" as a signed-in user with no
200+ // purchase permissions; any other method panics via the nil embedded interface.
201+ type sessionOnlyAuth struct { api.AuthServiceInterface }
202+
203+ func (sessionOnlyAuth ) ValidateSession (_ context.Context , token string ) (* api.Session , error ) {
204+ if token != "sess-tok" {
205+ return nil , errors .New ("invalid session" )
206+ }
207+ return & api.Session {UserID : "user-1" , Email : "user@example.com" }, nil
208+ }
209+
210+ func (sessionOnlyAuth ) HasPermissionAPI (context.Context , string , string , string ) (bool , error ) {
211+ return false , nil
212+ }
213+
214+ func (sessionOnlyAuth ) GetAllowedAccountsAPI (context.Context , string ) ([]string , error ) {
215+ return nil , nil
216+ }
0 commit comments