login_sources_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. // Copyright 2020 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package database
  5. import (
  6. "context"
  7. "testing"
  8. "time"
  9. mockrequire "github.com/derision-test/go-mockgen/testutil/require"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. "gorm.io/gorm"
  13. "gogs.io/gogs/internal/auth"
  14. "gogs.io/gogs/internal/auth/github"
  15. "gogs.io/gogs/internal/auth/ldap"
  16. "gogs.io/gogs/internal/auth/pam"
  17. "gogs.io/gogs/internal/auth/smtp"
  18. "gogs.io/gogs/internal/errutil"
  19. )
  20. func TestLoginSource_BeforeSave(t *testing.T) {
  21. now := time.Now()
  22. db := &gorm.DB{
  23. Config: &gorm.Config{
  24. SkipDefaultTransaction: true,
  25. NowFunc: func() time.Time {
  26. return now
  27. },
  28. },
  29. }
  30. t.Run("Config has not been set", func(t *testing.T) {
  31. s := &LoginSource{}
  32. err := s.BeforeSave(db)
  33. require.NoError(t, err)
  34. assert.Empty(t, s.Config)
  35. })
  36. t.Run("Config has been set", func(t *testing.T) {
  37. s := &LoginSource{
  38. Provider: pam.NewProvider(&pam.Config{
  39. ServiceName: "pam_service",
  40. }),
  41. }
  42. err := s.BeforeSave(db)
  43. require.NoError(t, err)
  44. assert.Equal(t, `{"ServiceName":"pam_service"}`, s.Config)
  45. })
  46. }
  47. func TestLoginSource_BeforeCreate(t *testing.T) {
  48. now := time.Now()
  49. db := &gorm.DB{
  50. Config: &gorm.Config{
  51. SkipDefaultTransaction: true,
  52. NowFunc: func() time.Time {
  53. return now
  54. },
  55. },
  56. }
  57. t.Run("CreatedUnix has been set", func(t *testing.T) {
  58. s := &LoginSource{
  59. CreatedUnix: 1,
  60. }
  61. _ = s.BeforeCreate(db)
  62. assert.Equal(t, int64(1), s.CreatedUnix)
  63. assert.Equal(t, int64(0), s.UpdatedUnix)
  64. })
  65. t.Run("CreatedUnix has not been set", func(t *testing.T) {
  66. s := &LoginSource{}
  67. _ = s.BeforeCreate(db)
  68. assert.Equal(t, db.NowFunc().Unix(), s.CreatedUnix)
  69. assert.Equal(t, db.NowFunc().Unix(), s.UpdatedUnix)
  70. })
  71. }
  72. func TestLoginSource_BeforeUpdate(t *testing.T) {
  73. now := time.Now()
  74. db := &gorm.DB{
  75. Config: &gorm.Config{
  76. SkipDefaultTransaction: true,
  77. NowFunc: func() time.Time {
  78. return now
  79. },
  80. },
  81. }
  82. s := &LoginSource{}
  83. _ = s.BeforeUpdate(db)
  84. assert.Equal(t, db.NowFunc().Unix(), s.UpdatedUnix)
  85. }
  86. func TestLoginSource_AfterFind(t *testing.T) {
  87. now := time.Now()
  88. db := &gorm.DB{
  89. Config: &gorm.Config{
  90. SkipDefaultTransaction: true,
  91. NowFunc: func() time.Time {
  92. return now
  93. },
  94. },
  95. }
  96. tests := []struct {
  97. name string
  98. authType auth.Type
  99. wantType any
  100. }{
  101. {
  102. name: "LDAP",
  103. authType: auth.LDAP,
  104. wantType: &ldap.Provider{},
  105. },
  106. {
  107. name: "DLDAP",
  108. authType: auth.DLDAP,
  109. wantType: &ldap.Provider{},
  110. },
  111. {
  112. name: "SMTP",
  113. authType: auth.SMTP,
  114. wantType: &smtp.Provider{},
  115. },
  116. {
  117. name: "PAM",
  118. authType: auth.PAM,
  119. wantType: &pam.Provider{},
  120. },
  121. {
  122. name: "GitHub",
  123. authType: auth.GitHub,
  124. wantType: &github.Provider{},
  125. },
  126. }
  127. for _, test := range tests {
  128. t.Run(test.name, func(t *testing.T) {
  129. s := LoginSource{
  130. Type: test.authType,
  131. Config: `{}`,
  132. CreatedUnix: now.Unix(),
  133. UpdatedUnix: now.Unix(),
  134. }
  135. err := s.AfterFind(db)
  136. require.NoError(t, err)
  137. assert.Equal(t, s.CreatedUnix, s.Created.Unix())
  138. assert.Equal(t, s.UpdatedUnix, s.Updated.Unix())
  139. assert.IsType(t, test.wantType, s.Provider)
  140. })
  141. }
  142. }
  143. func TestLoginSources(t *testing.T) {
  144. if testing.Short() {
  145. t.Skip()
  146. }
  147. t.Parallel()
  148. ctx := context.Background()
  149. s := &LoginSourcesStore{
  150. db: newTestDB(t, "LoginSourcesStore"),
  151. }
  152. for _, tc := range []struct {
  153. name string
  154. test func(t *testing.T, ctx context.Context, s *LoginSourcesStore)
  155. }{
  156. {"Create", loginSourcesCreate},
  157. {"Count", loginSourcesCount},
  158. {"DeleteByID", loginSourcesDeleteByID},
  159. {"GetByID", loginSourcesGetByID},
  160. {"List", loginSourcesList},
  161. {"ResetNonDefault", loginSourcesResetNonDefault},
  162. {"Save", loginSourcesSave},
  163. } {
  164. t.Run(tc.name, func(t *testing.T) {
  165. t.Cleanup(func() {
  166. err := clearTables(t, s.db)
  167. require.NoError(t, err)
  168. })
  169. tc.test(t, ctx, s)
  170. })
  171. if t.Failed() {
  172. break
  173. }
  174. }
  175. }
  176. func loginSourcesCreate(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  177. // Create first login source with name "GitHub"
  178. source, err := s.Create(ctx,
  179. CreateLoginSourceOptions{
  180. Type: auth.GitHub,
  181. Name: "GitHub",
  182. Activated: true,
  183. Default: false,
  184. Config: &github.Config{
  185. APIEndpoint: "https://api.github.com",
  186. },
  187. },
  188. )
  189. require.NoError(t, err)
  190. // Get it back and check the Created field
  191. source, err = s.GetByID(ctx, source.ID)
  192. require.NoError(t, err)
  193. assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), source.Created.UTC().Format(time.RFC3339))
  194. assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), source.Updated.UTC().Format(time.RFC3339))
  195. // Try to create second login source with same name should fail.
  196. _, err = s.Create(ctx, CreateLoginSourceOptions{Name: source.Name})
  197. wantErr := ErrLoginSourceAlreadyExist{args: errutil.Args{"name": source.Name}}
  198. assert.Equal(t, wantErr, err)
  199. }
  200. func setMockLoginSourceFilesStore(t *testing.T, s *LoginSourcesStore, mock loginSourceFilesStore) {
  201. before := s.files
  202. s.files = mock
  203. t.Cleanup(func() {
  204. s.files = before
  205. })
  206. }
  207. func loginSourcesCount(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  208. // Create two login sources, one in database and one as source file.
  209. _, err := s.Create(ctx,
  210. CreateLoginSourceOptions{
  211. Type: auth.GitHub,
  212. Name: "GitHub",
  213. Activated: true,
  214. Default: false,
  215. Config: &github.Config{
  216. APIEndpoint: "https://api.github.com",
  217. },
  218. },
  219. )
  220. require.NoError(t, err)
  221. mock := NewMockLoginSourceFilesStore()
  222. mock.LenFunc.SetDefaultReturn(2)
  223. setMockLoginSourceFilesStore(t, s, mock)
  224. assert.Equal(t, int64(3), s.Count(ctx))
  225. }
  226. func loginSourcesDeleteByID(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  227. t.Run("delete but in used", func(t *testing.T) {
  228. source, err := s.Create(ctx,
  229. CreateLoginSourceOptions{
  230. Type: auth.GitHub,
  231. Name: "GitHub",
  232. Activated: true,
  233. Default: false,
  234. Config: &github.Config{
  235. APIEndpoint: "https://api.github.com",
  236. },
  237. },
  238. )
  239. require.NoError(t, err)
  240. // Create a user that uses this login source
  241. _, err = newUsersStore(s.db).Create(ctx, "alice", "",
  242. CreateUserOptions{
  243. LoginSource: source.ID,
  244. },
  245. )
  246. require.NoError(t, err)
  247. // Delete the login source will result in error
  248. err = s.DeleteByID(ctx, source.ID)
  249. wantErr := ErrLoginSourceInUse{args: errutil.Args{"id": source.ID}}
  250. assert.Equal(t, wantErr, err)
  251. })
  252. mock := NewMockLoginSourceFilesStore()
  253. mock.GetByIDFunc.SetDefaultHook(func(id int64) (*LoginSource, error) {
  254. return nil, ErrLoginSourceNotExist{args: errutil.Args{"id": id}}
  255. })
  256. setMockLoginSourceFilesStore(t, s, mock)
  257. // Create a login source with name "GitHub2"
  258. source, err := s.Create(ctx,
  259. CreateLoginSourceOptions{
  260. Type: auth.GitHub,
  261. Name: "GitHub2",
  262. Activated: true,
  263. Default: false,
  264. Config: &github.Config{
  265. APIEndpoint: "https://api.github.com",
  266. },
  267. },
  268. )
  269. require.NoError(t, err)
  270. // Delete a non-existent ID is noop
  271. err = s.DeleteByID(ctx, 9999)
  272. require.NoError(t, err)
  273. // We should be able to get it back
  274. _, err = s.GetByID(ctx, source.ID)
  275. require.NoError(t, err)
  276. // Now delete this login source with ID
  277. err = s.DeleteByID(ctx, source.ID)
  278. require.NoError(t, err)
  279. // We should get token not found error
  280. _, err = s.GetByID(ctx, source.ID)
  281. wantErr := ErrLoginSourceNotExist{args: errutil.Args{"id": source.ID}}
  282. assert.Equal(t, wantErr, err)
  283. }
  284. func loginSourcesGetByID(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  285. mock := NewMockLoginSourceFilesStore()
  286. mock.GetByIDFunc.SetDefaultHook(func(id int64) (*LoginSource, error) {
  287. if id != 101 {
  288. return nil, ErrLoginSourceNotExist{args: errutil.Args{"id": id}}
  289. }
  290. return &LoginSource{ID: id}, nil
  291. })
  292. setMockLoginSourceFilesStore(t, s, mock)
  293. expConfig := &github.Config{
  294. APIEndpoint: "https://api.github.com",
  295. }
  296. // Create a login source with name "GitHub"
  297. source, err := s.Create(ctx,
  298. CreateLoginSourceOptions{
  299. Type: auth.GitHub,
  300. Name: "GitHub",
  301. Activated: true,
  302. Default: false,
  303. Config: expConfig,
  304. },
  305. )
  306. require.NoError(t, err)
  307. // Get the one in the database and test the read/write hooks
  308. source, err = s.GetByID(ctx, source.ID)
  309. require.NoError(t, err)
  310. assert.Equal(t, expConfig, source.Provider.Config())
  311. // Get the one in source file store
  312. _, err = s.GetByID(ctx, 101)
  313. require.NoError(t, err)
  314. }
  315. func loginSourcesList(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  316. mock := NewMockLoginSourceFilesStore()
  317. mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOptions) []*LoginSource {
  318. if opts.OnlyActivated {
  319. return []*LoginSource{
  320. {ID: 1},
  321. }
  322. }
  323. return []*LoginSource{
  324. {ID: 1},
  325. {ID: 2},
  326. }
  327. })
  328. setMockLoginSourceFilesStore(t, s, mock)
  329. // Create two login sources in database, one activated and the other one not
  330. _, err := s.Create(ctx,
  331. CreateLoginSourceOptions{
  332. Type: auth.PAM,
  333. Name: "PAM",
  334. Config: &pam.Config{
  335. ServiceName: "PAM",
  336. },
  337. },
  338. )
  339. require.NoError(t, err)
  340. _, err = s.Create(ctx,
  341. CreateLoginSourceOptions{
  342. Type: auth.GitHub,
  343. Name: "GitHub",
  344. Activated: true,
  345. Config: &github.Config{
  346. APIEndpoint: "https://api.github.com",
  347. },
  348. },
  349. )
  350. require.NoError(t, err)
  351. // List all login sources
  352. sources, err := s.List(ctx, ListLoginSourceOptions{})
  353. require.NoError(t, err)
  354. assert.Equal(t, 4, len(sources), "number of sources")
  355. // Only list activated login sources
  356. sources, err = s.List(ctx, ListLoginSourceOptions{OnlyActivated: true})
  357. require.NoError(t, err)
  358. assert.Equal(t, 2, len(sources), "number of sources")
  359. }
  360. func loginSourcesResetNonDefault(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  361. mock := NewMockLoginSourceFilesStore()
  362. mock.ListFunc.SetDefaultHook(func(opts ListLoginSourceOptions) []*LoginSource {
  363. mockFile := NewMockLoginSourceFileStore()
  364. mockFile.SetGeneralFunc.SetDefaultHook(func(name, value string) {
  365. assert.Equal(t, "is_default", name)
  366. assert.Equal(t, "false", value)
  367. })
  368. return []*LoginSource{
  369. {
  370. File: mockFile,
  371. },
  372. }
  373. })
  374. setMockLoginSourceFilesStore(t, s, mock)
  375. // Create two login sources both have default on
  376. source1, err := s.Create(ctx,
  377. CreateLoginSourceOptions{
  378. Type: auth.PAM,
  379. Name: "PAM",
  380. Default: true,
  381. Config: &pam.Config{
  382. ServiceName: "PAM",
  383. },
  384. },
  385. )
  386. require.NoError(t, err)
  387. source2, err := s.Create(ctx,
  388. CreateLoginSourceOptions{
  389. Type: auth.GitHub,
  390. Name: "GitHub",
  391. Activated: true,
  392. Default: true,
  393. Config: &github.Config{
  394. APIEndpoint: "https://api.github.com",
  395. },
  396. },
  397. )
  398. require.NoError(t, err)
  399. // Set source 1 as default
  400. err = s.ResetNonDefault(ctx, source1)
  401. require.NoError(t, err)
  402. // Verify the default state
  403. source1, err = s.GetByID(ctx, source1.ID)
  404. require.NoError(t, err)
  405. assert.True(t, source1.IsDefault)
  406. source2, err = s.GetByID(ctx, source2.ID)
  407. require.NoError(t, err)
  408. assert.False(t, source2.IsDefault)
  409. }
  410. func loginSourcesSave(t *testing.T, ctx context.Context, s *LoginSourcesStore) {
  411. t.Run("save to database", func(t *testing.T) {
  412. // Create a login source with name "GitHub"
  413. source, err := s.Create(ctx,
  414. CreateLoginSourceOptions{
  415. Type: auth.GitHub,
  416. Name: "GitHub",
  417. Activated: true,
  418. Default: false,
  419. Config: &github.Config{
  420. APIEndpoint: "https://api.github.com",
  421. },
  422. },
  423. )
  424. require.NoError(t, err)
  425. source.IsActived = false
  426. source.Provider = github.NewProvider(&github.Config{
  427. APIEndpoint: "https://api2.github.com",
  428. })
  429. err = s.Save(ctx, source)
  430. require.NoError(t, err)
  431. source, err = s.GetByID(ctx, source.ID)
  432. require.NoError(t, err)
  433. assert.False(t, source.IsActived)
  434. assert.Equal(t, "https://api2.github.com", source.GitHub().APIEndpoint)
  435. })
  436. t.Run("save to file", func(t *testing.T) {
  437. mockFile := NewMockLoginSourceFileStore()
  438. source := &LoginSource{
  439. Provider: github.NewProvider(&github.Config{
  440. APIEndpoint: "https://api.github.com",
  441. }),
  442. File: mockFile,
  443. }
  444. err := s.Save(ctx, source)
  445. require.NoError(t, err)
  446. mockrequire.Called(t, mockFile.SaveFunc)
  447. })
  448. }