httpserver: Add test coverage
continuous-integration/drone Build is passing
Details
continuous-integration/drone Build is passing
Details
This commit is contained in:
parent
8142349db4
commit
c441ec03f5
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: backend
|
||||
image: golang:1.18
|
||||
commands:
|
||||
- go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
- go build ./...
|
||||
- go vet ./...
|
||||
- staticcheck ./...
|
||||
- go test -bench=. -benchmem -cover ./...
|
|
@ -5,14 +5,14 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
type TwitterCredentials struct {
|
||||
ClientID, ClientSecret, CallbackURL string
|
||||
type TwitterConfig struct {
|
||||
ClientID, ClientSecret, CallbackURL, AuthorizeURL, TokenURL string
|
||||
}
|
||||
type Config struct {
|
||||
PublicPath string
|
||||
SessionKey string
|
||||
ListenAddr string
|
||||
Twitter TwitterCredentials
|
||||
Twitter TwitterConfig
|
||||
}
|
||||
|
||||
func NewFromEnv() (Config, error) {
|
||||
|
@ -28,10 +28,12 @@ func NewFromEnv() (Config, error) {
|
|||
PublicPath: os.Getenv("ELON_PUBLIC_PATH"),
|
||||
SessionKey: sessionKey,
|
||||
ListenAddr: listenAddr,
|
||||
Twitter: TwitterCredentials{
|
||||
Twitter: TwitterConfig{
|
||||
ClientID: os.Getenv("ELON_TWITTER_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("ELON_TWITTER_CLIENT_SECRET"),
|
||||
CallbackURL: os.Getenv("ELON_TWITTER_CALLBACK_URL"),
|
||||
AuthorizeURL: os.Getenv("ELON_TWITTER_AUTHORIZE_URL"),
|
||||
TokenURL: os.Getenv("ELON_TWITTER_TOKEN_URL"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
// Code generated by mockery v2.12.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
|
||||
sessions "github.com/gorilla/sessions"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
testing "testing"
|
||||
)
|
||||
|
||||
// Store is an autogenerated mock type for the Store type
|
||||
type Store struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: r, name
|
||||
func (_m *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
ret := _m.Called(r, name)
|
||||
|
||||
var r0 *sessions.Session
|
||||
if rf, ok := ret.Get(0).(func(*http.Request, string) *sessions.Session); ok {
|
||||
r0 = rf(r, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*sessions.Session)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok {
|
||||
r1 = rf(r, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// New provides a mock function with given fields: r, name
|
||||
func (_m *Store) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||
ret := _m.Called(r, name)
|
||||
|
||||
var r0 *sessions.Session
|
||||
if rf, ok := ret.Get(0).(func(*http.Request, string) *sessions.Session); ok {
|
||||
r0 = rf(r, name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*sessions.Session)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*http.Request, string) error); ok {
|
||||
r1 = rf(r, name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: r, w, s
|
||||
func (_m *Store) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error {
|
||||
ret := _m.Called(r, w, s)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*http.Request, http.ResponseWriter, *sessions.Session) error); ok {
|
||||
r0 = rf(r, w, s)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewStore creates a new instance of Store. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
func NewStore(t testing.TB) *Store {
|
||||
mock := &Store{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
10
go.mod
10
go.mod
|
@ -4,15 +4,23 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/go-chi/chi v1.5.4
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.1.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
|
39
go.sum
39
go.sum
|
@ -33,6 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
|
@ -40,6 +42,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
|||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
|
@ -107,22 +111,43 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -158,6 +183,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -184,6 +210,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -201,6 +228,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -226,8 +254,12 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -278,6 +310,7 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -359,9 +392,15 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
|||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
package httpserver
|
||||
|
||||
//go:generate mockery --recursive --srcpkg github.com/gorilla/sessions --name Store --output ../generated/mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/config"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/gorilla/sessions"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionName = "elon_session"
|
||||
sessionKeyState = "state"
|
||||
sessionKeyPkceVerifier = "pkce_verifier"
|
||||
stateLen = 64
|
||||
pkceVerifierLen = 64
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
templates *template.Template
|
||||
oauth2Config *oauth2.Config
|
||||
sessionStore sessions.Store
|
||||
tokenGenerator TokenGenerator
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewHandler(cfg config.Config, templates *template.Template, sessionStore sessions.Store, tokenGenerator TokenGenerator, logger *zap.Logger) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(loggerMiddleware(logger))
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
h := handler{
|
||||
templates: templates,
|
||||
oauth2Config: &oauth2.Config{
|
||||
ClientID: cfg.Twitter.ClientID,
|
||||
ClientSecret: cfg.Twitter.ClientSecret,
|
||||
RedirectURL: cfg.Twitter.CallbackURL,
|
||||
Scopes: []string{"tweet.read", "tweet.write", "users.read", "offline.access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: cfg.Twitter.AuthorizeURL,
|
||||
TokenURL: cfg.Twitter.TokenURL,
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
},
|
||||
},
|
||||
sessionStore: sessionStore,
|
||||
tokenGenerator: tokenGenerator,
|
||||
logger: logger.Sugar(),
|
||||
}
|
||||
|
||||
r.Get("/", h.getIndex)
|
||||
r.Get("/login", h.getLogin)
|
||||
r.Get("/callback", h.getCallback)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *handler) getIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.templates.ExecuteTemplate(w, "index", nil); err != nil {
|
||||
h.logger.With("err", err).Error("error rendering template")
|
||||
http.Error(w, "error rendering template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state := h.tokenGenerator.GenerateToken(stateLen)
|
||||
pkceVerifier := h.tokenGenerator.GenerateToken(pkceVerifierLen)
|
||||
|
||||
session, _ := h.sessionStore.Get(r, sessionName)
|
||||
session.Values[sessionKeyState] = state
|
||||
session.Values[sessionKeyPkceVerifier] = pkceVerifier
|
||||
if err := session.Save(r, w); err != nil {
|
||||
h.logger.With("err", err).Error("error saving session")
|
||||
http.Error(w, "unexpected error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
url := h.oauth2Config.AuthCodeURL(
|
||||
state,
|
||||
oauth2.SetAuthURLParam("code_challenge", encodeSHA256(pkceVerifier)),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
)
|
||||
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) getCallback(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := h.sessionStore.Get(r, sessionName)
|
||||
if err != nil {
|
||||
h.logger.With("err", err).Error("error reading session")
|
||||
http.Error(w, "error reading session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state, ok := session.Values[sessionKeyState]
|
||||
if !ok || state == "" {
|
||||
h.logger.Error("empty state parameter in oauth2 request")
|
||||
http.Error(w, "error validating request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if state != r.URL.Query().Get("state") {
|
||||
h.logger.Error("unexpected state in oauth2 request")
|
||||
http.Error(w, "error validating request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
h.logger.Error("empty code in oauth2 request")
|
||||
http.Error(w, "invalid code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pkceVerifier, ok := session.Values[sessionKeyPkceVerifier]
|
||||
if !ok || pkceVerifier == "" {
|
||||
h.logger.Error("no pkce verifier found in session")
|
||||
http.Error(w, "error reading session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.oauth2Config.Exchange(context.Background(), code, oauth2.SetAuthURLParam("code_verifier", pkceVerifier.(string)))
|
||||
if err != nil {
|
||||
h.logger.With("err", err).Error("error exchanging code for access token")
|
||||
http.Error(w, "error exchanging code", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
client := h.oauth2Config.Client(context.Background(), token)
|
||||
resp, err := client.Get("https://api.twitter.com/2/users/me")
|
||||
if err != nil {
|
||||
h.logger.With("err", err).Error("error fetching user")
|
||||
http.Error(w, "error fetching user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// TODO: do something sensible
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err = w.Write([]byte(body)); err != nil {
|
||||
h.logger.With("err", err).Error("error writing response")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
package httpserver_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/config"
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/generated/mocks"
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/httpserver"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var templates = template.Must(template.ParseGlob(filepath.Join("..", "public", "views", "*.html")))
|
||||
|
||||
// mockTokenGenerator implements httpserver.TokenGenerator.
|
||||
type mockTokenGenerator struct {
|
||||
i int
|
||||
tokens []string
|
||||
}
|
||||
|
||||
func (g *mockTokenGenerator) GenerateToken(_ int) string {
|
||||
i := g.i
|
||||
if len(g.tokens) <= i {
|
||||
return ""
|
||||
}
|
||||
g.i++
|
||||
return g.tokens[i]
|
||||
}
|
||||
|
||||
func TestGetIndex(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler := httpserver.NewHandler(
|
||||
config.Config{},
|
||||
templates,
|
||||
&mocks.Store{},
|
||||
&mockTokenGenerator{},
|
||||
zap.NewNop(),
|
||||
)
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.Contains(t, string(body), "Sign in with Twitter")
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
sessionSaveError error
|
||||
wantStatusCode int
|
||||
wantLocation string
|
||||
wantRespBody string
|
||||
}{
|
||||
{
|
||||
name: "successful login",
|
||||
wantStatusCode: http.StatusFound,
|
||||
wantLocation: "https://www.example.com/oauth/authorize?client_id=foo&code_challenge=RdoE4fOeAO8YelxeEEd70qNDoVgzl4844utzxsozlR4&code_challenge_method=S256&redirect_uri=https%3A%2F%2Fwww.example.com%2Fcallback&response_type=code&scope=tweet.read+tweet.write+users.read+offline.access&state=state",
|
||||
},
|
||||
{
|
||||
name: "error saving session",
|
||||
wantStatusCode: http.StatusInternalServerError,
|
||||
sessionSaveError: errors.New("boom"),
|
||||
wantRespBody: "unexpected error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mockStore mocks.Store
|
||||
sess := sessions.NewSession(&mockStore, "elon_session")
|
||||
mockStore.On("Get", mock.Anything, "elon_session").Return(sess, nil)
|
||||
mockStore.On("Save", mock.Anything, mock.Anything, sess).Return(tc.sessionSaveError)
|
||||
|
||||
handler := httpserver.NewHandler(
|
||||
config.Config{
|
||||
Twitter: config.TwitterConfig{
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
CallbackURL: "https://www.example.com/callback",
|
||||
AuthorizeURL: "https://www.example.com/oauth/authorize",
|
||||
TokenURL: "https://www.example.com/oauth/token",
|
||||
},
|
||||
},
|
||||
templates,
|
||||
&mockStore,
|
||||
&mockTokenGenerator{tokens: []string{"state", "pkceVerifier"}},
|
||||
zap.NewNop(),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/login", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.wantStatusCode, res.StatusCode)
|
||||
if tc.wantRespBody == "" {
|
||||
assert.Equal(t, tc.wantLocation, res.Header.Get("Location"))
|
||||
}
|
||||
|
||||
if tc.wantRespBody != "" {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), tc.wantRespBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallback(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
state string
|
||||
sessionState string
|
||||
sessionPkceVerifier string
|
||||
code string
|
||||
sessionReadError error
|
||||
oauth2StatusCode int
|
||||
wantStatusCode int
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "unable to read session",
|
||||
sessionReadError: errors.New("boom"),
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "error reading session",
|
||||
},
|
||||
{
|
||||
name: "missing state",
|
||||
state: "mystate",
|
||||
sessionState: "",
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "error validating request",
|
||||
},
|
||||
{
|
||||
name: "unexpected state",
|
||||
state: "mystate",
|
||||
sessionState: "foostate",
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "error validating request",
|
||||
},
|
||||
{
|
||||
name: "empty code",
|
||||
state: "mystate",
|
||||
sessionState: "mystate",
|
||||
code: "",
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "invalid code",
|
||||
},
|
||||
{
|
||||
name: "empty pkce verifier",
|
||||
state: "mystate",
|
||||
sessionState: "mystate",
|
||||
code: "mycode",
|
||||
sessionPkceVerifier: "",
|
||||
wantStatusCode: http.StatusBadRequest,
|
||||
wantError: "error reading session",
|
||||
},
|
||||
{
|
||||
name: "error exchanging code",
|
||||
state: "mystate",
|
||||
sessionState: "mystate",
|
||||
code: "mycode",
|
||||
sessionPkceVerifier: "mypkceverifier",
|
||||
oauth2StatusCode: http.StatusInternalServerError,
|
||||
wantStatusCode: http.StatusForbidden,
|
||||
wantError: "error exchanging code",
|
||||
},
|
||||
{
|
||||
name: "successful exchange",
|
||||
state: "mystate",
|
||||
sessionState: "mystate",
|
||||
code: "mycode",
|
||||
sessionPkceVerifier: "mypkceverifier",
|
||||
oauth2StatusCode: http.StatusOK,
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var mockStore mocks.Store
|
||||
sess := sessions.NewSession(&mockStore, "elon_session")
|
||||
sess.Values["state"] = tc.sessionState
|
||||
sess.Values["pkce_verifier"] = tc.sessionPkceVerifier
|
||||
mockStore.On("Get", mock.Anything, "elon_session").Return(sess, tc.sessionReadError)
|
||||
|
||||
const callbackURL = "https://www.example.com/callback"
|
||||
oauthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "/oauth/token", r.URL.Path)
|
||||
|
||||
require.NoError(t, r.ParseForm())
|
||||
assert.Equal(t, tc.code, r.PostFormValue("code"))
|
||||
assert.Equal(t, tc.sessionPkceVerifier, r.PostFormValue("code_verifier"))
|
||||
assert.Equal(t, "authorization_code", r.PostFormValue("grant_type"))
|
||||
assert.Equal(t, callbackURL, r.PostFormValue("redirect_uri"))
|
||||
|
||||
w.WriteHeader(tc.oauth2StatusCode)
|
||||
if tc.oauth2StatusCode == http.StatusOK {
|
||||
w.Write([]byte(`access_token=foo&expires_in=3600&refresh_token=&token_type=bearer"}`))
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(oauthHandler)
|
||||
|
||||
handler := httpserver.NewHandler(
|
||||
config.Config{
|
||||
Twitter: config.TwitterConfig{
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
CallbackURL: callbackURL,
|
||||
AuthorizeURL: srv.URL + "/oauth/authorize",
|
||||
TokenURL: srv.URL + "/oauth/token",
|
||||
},
|
||||
},
|
||||
templates,
|
||||
&mockStore,
|
||||
nil,
|
||||
zap.NewNop(),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/callback?state="+tc.state+"&code="+tc.code, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, tc.wantStatusCode, res.StatusCode)
|
||||
|
||||
if tc.wantError != "" {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), tc.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,176 +1 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/config"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionName = "elon_session"
|
||||
sessionKeyState = "state"
|
||||
sessionKeyPkceVerifier = "pkce_verifier"
|
||||
stateLen = 64
|
||||
pkceVerifierLen = 64
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
templates *template.Template
|
||||
oauth2Config *oauth2.Config
|
||||
sessionStore *sessions.CookieStore
|
||||
}
|
||||
|
||||
func newHandler(cfg config.Config, templates *template.Template) *handler {
|
||||
return &handler{
|
||||
templates: templates,
|
||||
sessionStore: sessions.NewCookieStore([]byte(cfg.SessionKey)),
|
||||
oauth2Config: &oauth2.Config{
|
||||
ClientID: cfg.Twitter.ClientID,
|
||||
ClientSecret: cfg.Twitter.ClientSecret,
|
||||
RedirectURL: cfg.Twitter.CallbackURL,
|
||||
Scopes: []string{"tweet.read", "tweet.write", "users.read", "offline.access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: "https://twitter.com/i/oauth2/authorize",
|
||||
TokenURL: "https://api.twitter.com/2/oauth2/token",
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.templates.ExecuteTemplate(w, "index", nil); err != nil {
|
||||
log.Printf("error rendering template: %v", err)
|
||||
http.Error(w, "error rendering template", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state := randSeq(stateLen)
|
||||
pkceVerifier := randSeq(pkceVerifierLen)
|
||||
|
||||
session, _ := h.sessionStore.Get(r, sessionName)
|
||||
session.Values[sessionKeyState] = state
|
||||
session.Values[sessionKeyPkceVerifier] = pkceVerifier
|
||||
if err := session.Save(r, w); err != nil {
|
||||
log.Printf("error saving session: %v", err)
|
||||
http.Error(w, "error saving session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
url := h.oauth2Config.AuthCodeURL(
|
||||
state,
|
||||
oauth2.SetAuthURLParam("code_challenge", encodeSHA256(pkceVerifier)),
|
||||
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
|
||||
)
|
||||
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (h *handler) getCallback(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := h.sessionStore.Get(r, sessionName)
|
||||
if err != nil {
|
||||
log.Printf("error reading session: %v", err)
|
||||
http.Error(w, "error reading session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state, ok := session.Values[sessionKeyState]
|
||||
if !ok {
|
||||
log.Println("empty state", err)
|
||||
http.Error(w, "error reading session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if state != r.URL.Query().Get("state") {
|
||||
http.Error(w, "error validating request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, "empty code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
pkceVerifier, ok := session.Values[sessionKeyPkceVerifier]
|
||||
if !ok {
|
||||
log.Println("empty code challenge", err)
|
||||
http.Error(w, "error reading session", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.oauth2Config.Exchange(context.Background(), code, oauth2.SetAuthURLParam("code_verifier", pkceVerifier.(string)))
|
||||
if err != nil {
|
||||
log.Printf("error exchanging code: %v", err)
|
||||
http.Error(w, "error exchanging code", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
client := h.oauth2Config.Client(context.Background(), token)
|
||||
resp, err := client.Get("https://api.twitter.com/2/users/me")
|
||||
if err != nil {
|
||||
log.Printf("error fetching users/me: %v", err)
|
||||
http.Error(w, "error fetching user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// TODO: do something sensible
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
w.Header().Set("content-type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err = w.Write([]byte(body)); err != nil {
|
||||
log.Printf("error writing response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Start(cfg config.Config) error {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
templates, err := template.ParseGlob(filepath.Join(cfg.PublicPath, "views", "*.html"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading templates: %v", err)
|
||||
}
|
||||
|
||||
h := newHandler(cfg, templates)
|
||||
r.Get("/", h.getIndex)
|
||||
r.Get("/login", h.getLogin)
|
||||
r.Get("/callback", h.getCallback)
|
||||
|
||||
return http.ListenAndServe(":8000", r)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randSeq(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func encodeSHA256(s string) string {
|
||||
enc := sha256.Sum256([]byte(s))
|
||||
return base64.RawURLEncoding.EncodeToString(enc[:])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func loggerMiddleware(l *zap.Logger) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
|
||||
t1 := time.Now()
|
||||
defer func() {
|
||||
l.Info("HTTP",
|
||||
zap.String("proto", r.Proto),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Duration("dur", time.Since(t1)),
|
||||
zap.Int("status", ww.Status()),
|
||||
zap.Int("size", ww.BytesWritten()),
|
||||
zap.String("reqId", middleware.GetReqID(r.Context())))
|
||||
}()
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package httpserver
|
|
@ -0,0 +1,33 @@
|
|||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
type TokenGenerator interface {
|
||||
GenerateToken(n int) string
|
||||
}
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
type RandomTokenGenerator struct{}
|
||||
|
||||
func (g RandomTokenGenerator) GenerateToken(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func encodeSHA256(s string) string {
|
||||
enc := sha256.Sum256([]byte(s))
|
||||
return base64.RawURLEncoding.EncodeToString(enc[:])
|
||||
}
|
30
main.go
30
main.go
|
@ -1,21 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/config"
|
||||
"git.netflux.io/rob/elon-eats-my-tweets/httpserver"
|
||||
"github.com/gorilla/sessions"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
c, err := config.NewFromEnv()
|
||||
cfg, err := config.NewFromEnv()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Fatal(httpserver.Start(c))
|
||||
logger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
templates, err := template.ParseGlob(filepath.Join(cfg.PublicPath, "views", "*.html"))
|
||||
if err != nil {
|
||||
log.Fatalf("error loading templates: %v", err)
|
||||
}
|
||||
|
||||
handler := httpserver.NewHandler(
|
||||
cfg,
|
||||
templates,
|
||||
sessions.NewCookieStore([]byte(cfg.SessionKey)),
|
||||
httpserver.RandomTokenGenerator{},
|
||||
logger,
|
||||
)
|
||||
log.Fatal(http.ListenAndServe(cfg.ListenAddr, handler))
|
||||
}
|
||||
|
|
Reference in New Issue