// Copyright 2026 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: GPL-3.0-or-later package shared import ( "bytes" "fmt" "net/http" "net/http/httptest" "testing" "time" auth_model "forgejo.org/models/auth" "forgejo.org/models/db" "forgejo.org/models/unittest" "forgejo.org/modules/json" "forgejo.org/modules/jwtx" "forgejo.org/modules/test" "forgejo.org/modules/web" "forgejo.org/routers/common" "forgejo.org/services/auth/source/oauth2" "forgejo.org/services/authz" "forgejo.org/services/context" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestReducer(t *testing.T) { defer unittest.OverrideFixtures("routers/api/shared/TestReducer")() require.NoError(t, unittest.PrepareTestDatabase()) makeRecorder := func() *httptest.ResponseRecorder { buff := bytes.NewBufferString("") recorder := httptest.NewRecorder() recorder.Body = buff return recorder } r := web.NewRoute() r.Use(common.ProtocolMiddlewares()...) r.Use(Middlewares()...) type ReducerInfo struct { IsSigned bool IsNil bool IsAllAccess bool IsPublicAccess bool IsSpecificAccess bool } r.Get("/api/test", func(ctx *context.APIContext) { retval := ReducerInfo{ IsSigned: ctx.IsSigned, IsNil: ctx.Reducer == nil, } _, isAllAccess := ctx.Reducer.(*authz.AllAccessAuthorizationReducer) retval.IsAllAccess = isAllAccess _, isPublicAccess := ctx.Reducer.(*authz.PublicReposAuthorizationReducer) retval.IsPublicAccess = isPublicAccess _, isSpecificAccess := ctx.Reducer.(*authz.SpecificReposAuthorizationReducer) retval.IsSpecificAccess = isSpecificAccess ctx.JSON(http.StatusOK, retval) }) t.Run("Basic Auth w/ PAT", func(t *testing.T) { t.Run("unrestricted access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.SetBasicAuth("token", "4a0c970da8bf58408a8c22264b2ac1ff47dadcce") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.True(t, reducerInfo.IsAllAccess) assert.False(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) t.Run("public-only access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.SetBasicAuth("token", "83909b5b978acc5620ae0c7b0e55b548da2e26b5") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.False(t, reducerInfo.IsAllAccess) assert.True(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) t.Run("specific-repo access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.SetBasicAuth("token", "46088605ec804b43ebd15cef1b3f210c31b066dd") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.False(t, reducerInfo.IsAllAccess) assert.False(t, reducerInfo.IsPublicAccess) assert.True(t, reducerInfo.IsSpecificAccess) }) }) t.Run("Token Auth", func(t *testing.T) { t.Run("unrestricted access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.Header.Set("Authorization", "token 4a0c970da8bf58408a8c22264b2ac1ff47dadcce") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.True(t, reducerInfo.IsAllAccess) assert.False(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) t.Run("public-only access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.Header.Set("Authorization", "token 83909b5b978acc5620ae0c7b0e55b548da2e26b5") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.False(t, reducerInfo.IsAllAccess) assert.True(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) t.Run("specific-repo access token", func(t *testing.T) { recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.Header.Set("Authorization", "token 46088605ec804b43ebd15cef1b3f210c31b066dd") require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.False(t, reducerInfo.IsAllAccess) assert.False(t, reducerInfo.IsPublicAccess) assert.True(t, reducerInfo.IsSpecificAccess) }) }) t.Run("OAuth", func(t *testing.T) { signingKey, err := jwtx.CreateSigningKey("HS256", make([]byte, 32)) require.NoError(t, err) defer test.MockVariableValue(&oauth2.DefaultSigningKey, signingKey)() t.Run("unrestricted grant", func(t *testing.T) { grant := &auth_model.OAuth2Grant{ UserID: 2, ApplicationID: 100, // fake, but required here for unique constraint Scope: "write:repository", } _, err = db.GetEngine(t.Context()).Insert(grant) require.NoError(t, err) token := oauth2.Token{ GrantID: grant.ID, Type: oauth2.TypeAccessToken, Counter: 100, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), }, } signed, err := token.SignToken(oauth2.DefaultSigningKey) require.NoError(t, err) recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed)) require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.True(t, reducerInfo.IsAllAccess) assert.False(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) t.Run("public-only grant", func(t *testing.T) { grant := &auth_model.OAuth2Grant{ UserID: 2, ApplicationID: 101, // fake, but required here for unique constraint Scope: "write:repository public-only", } _, err = db.GetEngine(t.Context()).Insert(grant) require.NoError(t, err) token := oauth2.Token{ GrantID: grant.ID, Type: oauth2.TypeAccessToken, Counter: 100, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), }, } signed, err := token.SignToken(oauth2.DefaultSigningKey) require.NoError(t, err) recorder := makeRecorder() req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil) req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed)) require.NoError(t, err) r.ServeHTTP(recorder, req) assert.Equal(t, http.StatusOK, recorder.Code) var reducerInfo ReducerInfo require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo)) assert.True(t, reducerInfo.IsSigned) assert.False(t, reducerInfo.IsNil) assert.False(t, reducerInfo.IsAllAccess) assert.True(t, reducerInfo.IsPublicAccess) assert.False(t, reducerInfo.IsSpecificAccess) }) }) }