| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- //go:build unit
- package handler
- import (
- "context"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- type oauthCaptchaSettingRepo struct {
- values map[string]string
- }
- func (r *oauthCaptchaSettingRepo) Get(context.Context, string) (*service.Setting, error) {
- return nil, service.ErrSettingNotFound
- }
- func (r *oauthCaptchaSettingRepo) GetValue(_ context.Context, key string) (string, error) {
- value, ok := r.values[key]
- if !ok {
- return "", service.ErrSettingNotFound
- }
- return value, nil
- }
- func (r *oauthCaptchaSettingRepo) Set(context.Context, string, string) error { return nil }
- func (r *oauthCaptchaSettingRepo) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
- values := make(map[string]string, len(keys))
- for _, key := range keys {
- if value, ok := r.values[key]; ok {
- values[key] = value
- }
- }
- return values, nil
- }
- func (r *oauthCaptchaSettingRepo) SetMultiple(context.Context, map[string]string) error {
- return nil
- }
- func (r *oauthCaptchaSettingRepo) GetAll(context.Context) (map[string]string, error) {
- return r.values, nil
- }
- func (r *oauthCaptchaSettingRepo) Delete(context.Context, string) error { return nil }
- type oauthCaptchaVerifier struct {
- calls int
- proof service.TencentCaptchaProof
- }
- func (v *oauthCaptchaVerifier) VerifyTicket(_ context.Context, _ service.TencentCaptchaCredentials, proof service.TencentCaptchaProof, _ string) (*service.TencentCaptchaVerifyResponse, error) {
- v.calls++
- v.proof = proof
- return &service.TencentCaptchaVerifyResponse{CaptchaCode: 1}, nil
- }
- func newOAuthCaptchaTestHandler(enabled bool) (*AuthHandler, *oauthCaptchaVerifier) {
- values := map[string]string{}
- if enabled {
- values = map[string]string{
- service.SettingKeyTencentCaptchaEnabled: "true",
- service.SettingKeyTencentCaptchaAppID: "123456789",
- service.SettingKeyTencentCaptchaAppSecretKey: "app-secret",
- service.SettingKeyTencentCaptchaCloudSecretID: "cloud-secret-id",
- service.SettingKeyTencentCaptchaCloudSecretKey: "cloud-secret-key",
- }
- }
- cfg := &config.Config{}
- settings := service.NewSettingService(&oauthCaptchaSettingRepo{values: values}, cfg)
- verifier := &oauthCaptchaVerifier{}
- authService := service.NewAuthService(nil, nil, nil, nil, cfg, settings, nil, nil, nil, nil, nil, nil, nil)
- authService.SetTencentCaptchaService(service.NewTencentCaptchaService(settings, verifier))
- return &AuthHandler{authService: authService, settingSvc: settings, cfg: cfg}, verifier
- }
- func oauthStartHandlers() map[string]func(*AuthHandler, *gin.Context) {
- return map[string]func(*AuthHandler, *gin.Context){
- "github": func(h *AuthHandler, c *gin.Context) { h.GitHubOAuthStart(c) },
- "google": func(h *AuthHandler, c *gin.Context) { h.GoogleOAuthStart(c) },
- "linuxdo": func(h *AuthHandler, c *gin.Context) { h.LinuxDoOAuthStart(c) },
- "dingtalk": func(h *AuthHandler, c *gin.Context) { h.DingTalkOAuthStart(c) },
- "wechat": func(h *AuthHandler, c *gin.Context) { h.WeChatOAuthStart(c) },
- "oidc": func(h *AuthHandler, c *gin.Context) { h.OIDCOAuthStart(c) },
- }
- }
- func TestOAuthStartGetRejectsAnonymousLoginWhenTencentEnabledWithoutSideEffects(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for provider, start := range oauthStartHandlers() {
- t.Run(provider, func(t *testing.T) {
- handler, verifier := newOAuthCaptchaTestHandler(true)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/"+provider+"/start?intent=bind_current_user", nil)
- start(handler, c)
- require.Equal(t, http.StatusBadRequest, recorder.Code)
- require.Contains(t, recorder.Body.String(), "TENCENT_CAPTCHA_VERIFICATION_FAILED")
- require.Empty(t, recorder.Header().Get("Location"))
- require.Empty(t, recorder.Header().Values("Set-Cookie"))
- require.Zero(t, verifier.calls)
- })
- }
- }
- func TestOAuthStartPostReturnsAuthorizeURLAfterTencentVerification(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for provider := range oauthStartHandlers() {
- t.Run(provider, func(t *testing.T) {
- handler, verifier := newOAuthCaptchaTestHandler(true)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(
- http.MethodPost,
- "/api/v1/auth/oauth/"+provider+"/start",
- strings.NewReader(`{"tencent_captcha_ticket":"ticket-value","tencent_captcha_randstr":"@rand-value"}`),
- )
- c.Request.Header.Set("Content-Type", "application/json")
- require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
- respondOAuthStart(c, "https://provider.example/authorize")
- require.Equal(t, http.StatusOK, recorder.Code)
- require.Contains(t, recorder.Body.String(), `"authorize_url":"https://provider.example/authorize"`)
- require.Equal(t, 1, verifier.calls)
- require.Equal(t, service.TencentCaptchaProof{Ticket: "ticket-value", Randstr: "@rand-value"}, verifier.proof)
- })
- }
- }
- func TestOAuthStartPostRequiresTencentProofWhenEnabled(t *testing.T) {
- gin.SetMode(gin.TestMode)
- for provider := range oauthStartHandlers() {
- t.Run(provider, func(t *testing.T) {
- handler, verifier := newOAuthCaptchaTestHandler(true)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/auth/oauth/"+provider+"/start", strings.NewReader(`{}`))
- c.Request.Header.Set("Content-Type", "application/json")
- require.False(t, handler.requireActionCaptchaForOAuthLoginStart(c))
- require.Equal(t, http.StatusBadRequest, recorder.Code)
- require.Contains(t, recorder.Body.String(), "TENCENT_CAPTCHA_VERIFICATION_FAILED")
- require.Zero(t, verifier.calls)
- })
- }
- }
- func TestOAuthBindingPathRemainsOutsideTencentGate(t *testing.T) {
- gin.SetMode(gin.TestMode)
- handler := &AuthHandler{}
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/oidc/bind/start", nil)
- require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
- require.Equal(t, http.StatusOK, recorder.Code)
- }
- func TestOAuthStartGetRemainsCompatibleWhenTencentDisabled(t *testing.T) {
- gin.SetMode(gin.TestMode)
- handler, verifier := newOAuthCaptchaTestHandler(false)
- recorder := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(recorder)
- c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/github/start", nil)
- require.True(t, handler.requireActionCaptchaForOAuthLoginStart(c))
- respondOAuthStart(c, "https://provider.example/authorize")
- require.Equal(t, http.StatusFound, recorder.Code)
- require.Equal(t, "https://provider.example/authorize", recorder.Header().Get("Location"))
- require.Zero(t, verifier.calls)
- }
|