Add omit_auth_context as a safe default for custom menu iframe pages, converge untrusted public output, and document the deployed shop menu configuration.
332 lines
12 KiB
Go
332 lines
12 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuNewItemDefaultsSafe(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{}, "")
|
|
|
|
rec := updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "shop",
|
|
"label": "Shop",
|
|
"url": "https://shop.example.com/pay",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotNil(t, items[0].OmitAuthContext)
|
|
require.True(t, *items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyMissingFieldPreservedForSameTrustedURL(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"id":"legacy","label":"Legacy","url":"https://app.example.com/internal","visibility":"user"}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "legacy",
|
|
"label": "Legacy Renamed",
|
|
"url": "https://app.example.com/internal",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyWithoutIDPreservedByFingerprint(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"label":"Legacy","url":"https://app.example.com/internal","visibility":"user"}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"label": "Legacy",
|
|
"url": "https://app.example.com/internal",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotEmpty(t, items[0].ID)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyUntrustedSameOriginPreserved(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"id":"legacy","label":"Legacy","url":"https://shop.example.com/old","visibility":"user"}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "legacy",
|
|
"label": "Legacy Renamed",
|
|
"url": "https://shop.example.com/new",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyMovedToUntrustedExternalBecomesSafe(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"id":"legacy","label":"Legacy","url":"https://app.example.com/internal","visibility":"user"}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "legacy",
|
|
"label": "Legacy",
|
|
"url": "https://shop.example.com/pay",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotNil(t, items[0].OmitAuthContext)
|
|
require.True(t, *items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuRejectsExplicitFalseForUntrustedURL(t *testing.T) {
|
|
_, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, "")
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusBadRequest, []map[string]any{{
|
|
"id": "leak",
|
|
"label": "Leak",
|
|
"url": "https://shop.example.com/pay",
|
|
"visibility": "user",
|
|
"omit_auth_context": false,
|
|
}})
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuStoredFrontendURLDoesNotSelfAuthorize(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{}, `[]`)
|
|
repo.values[service.SettingKeyFrontendURL] = "https://app.example.com"
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusBadRequest, []map[string]any{{
|
|
"id": "internal",
|
|
"label": "Internal",
|
|
"url": "https://app.example.com/internal",
|
|
"visibility": "user",
|
|
"omit_auth_context": false,
|
|
}})
|
|
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuTrustUsesEnvAllowlist(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Security: config.SecurityConfig{
|
|
CustomMenuAuthContextTrustedOrigins: []string{"https://app.example.com"},
|
|
},
|
|
}, `[]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "internal",
|
|
"label": "Internal",
|
|
"url": "https://app.example.com/internal",
|
|
"visibility": "user",
|
|
"omit_auth_context": false,
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotNil(t, items[0].OmitAuthContext)
|
|
require.False(t, *items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuNewItemsWithoutIDGetUniqueIDs(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{}, "")
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{
|
|
{
|
|
"label": "Shop One",
|
|
"url": "https://shop.example.com/one",
|
|
"visibility": "user",
|
|
},
|
|
{
|
|
"label": "Shop Two",
|
|
"url": "https://shop.example.com/two",
|
|
"visibility": "user",
|
|
},
|
|
})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 2)
|
|
require.NotEmpty(t, items[0].ID)
|
|
require.NotEmpty(t, items[1].ID)
|
|
require.NotEqual(t, items[0].ID, items[1].ID)
|
|
require.NotNil(t, items[0].OmitAuthContext)
|
|
require.True(t, *items[0].OmitAuthContext)
|
|
require.NotNil(t, items[1].OmitAuthContext)
|
|
require.True(t, *items[1].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyItemsWithoutIDRoundTrip(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[
|
|
{"label":"Legacy One","url":"https://app.example.com/one","visibility":"user"},
|
|
{"label":"Legacy Two","url":"https://app.example.com/two","visibility":"user"}
|
|
]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{
|
|
{
|
|
"label": "Legacy One Renamed",
|
|
"url": "https://app.example.com/one",
|
|
"visibility": "user",
|
|
},
|
|
{
|
|
"label": "Legacy Two Renamed",
|
|
"url": "https://app.example.com/two",
|
|
"visibility": "user",
|
|
},
|
|
})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 2)
|
|
require.NotEmpty(t, items[0].ID)
|
|
require.NotEmpty(t, items[1].ID)
|
|
require.NotEqual(t, items[0].ID, items[1].ID)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
require.Nil(t, items[1].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyWithoutIDRenamePreservesMissingField(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"label":"Legacy","url":"https://app.example.com/internal","visibility":"user"}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"label": "Legacy Renamed",
|
|
"url": "https://app.example.com/internal",
|
|
"visibility": "user",
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotEmpty(t, items[0].ID)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyExplicitFalseRoundTripPreserved(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"id":"legacy","label":"Legacy","url":"https://shop.example.com/old","visibility":"user","omit_auth_context":false}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusOK, []map[string]any{{
|
|
"id": "legacy",
|
|
"label": "Legacy Renamed",
|
|
"url": "https://shop.example.com/new",
|
|
"visibility": "user",
|
|
"omit_auth_context": false,
|
|
}})
|
|
|
|
items := storedCustomMenuItems(t, repo)
|
|
require.Len(t, items, 1)
|
|
require.NotNil(t, items[0].OmitAuthContext)
|
|
require.False(t, *items[0].OmitAuthContext)
|
|
}
|
|
|
|
func TestSettingHandler_UpdateSettings_CustomMenuLegacyExplicitFalseMovedToNewUntrustedRejected(t *testing.T) {
|
|
_, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[{"id":"legacy","label":"Legacy","url":"https://shop.example.com/old","visibility":"user","omit_auth_context":false}]`)
|
|
|
|
updateCustomMenuSettings(t, handler, http.StatusBadRequest, []map[string]any{{
|
|
"id": "legacy",
|
|
"label": "Legacy",
|
|
"url": "https://other.example.com/pay",
|
|
"visibility": "user",
|
|
"omit_auth_context": false,
|
|
}})
|
|
}
|
|
|
|
func TestSettingHandler_GetSettings_CustomMenuReturnsStoredValue(t *testing.T) {
|
|
repo, handler := newCustomMenuSettingHandler(t, &config.Config{
|
|
Server: config.ServerConfig{FrontendURL: "https://app.example.com"},
|
|
}, `[
|
|
{"id":"legacy","label":"Legacy","url":"https://shop.example.com/pay","visibility":"user"},
|
|
{"id":"unsafe","label":"Unsafe","url":"https://shop.example.com/pay","visibility":"admin","omit_auth_context":false},
|
|
{"id":"trusted","label":"Trusted","url":"https://app.example.com/internal","visibility":"user"}
|
|
]`)
|
|
_ = repo
|
|
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/settings", nil)
|
|
handler.GetSettings(c)
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code)
|
|
var resp response.Response
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
|
data := resp.Data.(map[string]any)
|
|
rawItems, err := json.Marshal(data["custom_menu_items"])
|
|
require.NoError(t, err)
|
|
|
|
var items []dto.CustomMenuItem
|
|
require.NoError(t, json.Unmarshal(rawItems, &items))
|
|
require.Len(t, items, 3)
|
|
require.Nil(t, items[0].OmitAuthContext)
|
|
require.NotNil(t, items[1].OmitAuthContext)
|
|
require.False(t, *items[1].OmitAuthContext)
|
|
require.Nil(t, items[2].OmitAuthContext)
|
|
}
|
|
|
|
func newCustomMenuSettingHandler(t *testing.T, cfg *config.Config, existingCustomMenuJSON string) (*settingHandlerRepoStub, *SettingHandler) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
values := map[string]string{
|
|
service.SettingKeyPromoCodeEnabled: "true",
|
|
}
|
|
if existingCustomMenuJSON != "" {
|
|
values[service.SettingKeyCustomMenuItems] = existingCustomMenuJSON
|
|
}
|
|
repo := &settingHandlerRepoStub{values: values}
|
|
svc := service.NewSettingService(repo, cfg)
|
|
return repo, NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)
|
|
}
|
|
|
|
func updateCustomMenuSettings(t *testing.T, handler *SettingHandler, wantStatus int, items []map[string]any) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
body := map[string]any{
|
|
"promo_code_enabled": true,
|
|
"custom_menu_items": items,
|
|
}
|
|
rawBody, err := json.Marshal(body)
|
|
require.NoError(t, err)
|
|
|
|
rec := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(rec)
|
|
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
handler.UpdateSettings(c)
|
|
|
|
require.Equal(t, wantStatus, rec.Code, rec.Body.String())
|
|
return rec
|
|
}
|
|
|
|
func storedCustomMenuItems(t *testing.T, repo *settingHandlerRepoStub) []dto.CustomMenuItem {
|
|
t.Helper()
|
|
raw := repo.values[service.SettingKeyCustomMenuItems]
|
|
require.NotEmpty(t, raw)
|
|
var items []dto.CustomMenuItem
|
|
require.NoError(t, json.Unmarshal([]byte(raw), &items))
|
|
return items
|
|
}
|