project_handler_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. package api_test
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "reflect"
  6. "strings"
  7. "testing"
  8. "github.com/go-test/deep"
  9. "github.com/porter-dev/porter/internal/forms"
  10. "github.com/porter-dev/porter/internal/models"
  11. )
  12. // ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
  13. type projTest struct {
  14. initializers []func(t *tester)
  15. msg string
  16. method string
  17. endpoint string
  18. body string
  19. expStatus int
  20. expBody string
  21. useCookie bool
  22. validators []func(c *projTest, tester *tester, t *testing.T)
  23. }
  24. func testProjRequests(t *testing.T, tests []*projTest, canQuery bool) {
  25. for _, c := range tests {
  26. // create a new tester
  27. tester := newTester(canQuery)
  28. // if there's an initializer, call it
  29. for _, init := range c.initializers {
  30. init(tester)
  31. }
  32. req, err := http.NewRequest(
  33. c.method,
  34. c.endpoint,
  35. strings.NewReader(c.body),
  36. )
  37. tester.req = req
  38. if c.useCookie {
  39. req.AddCookie(tester.cookie)
  40. }
  41. if err != nil {
  42. t.Fatal(err)
  43. }
  44. tester.execute()
  45. rr := tester.rr
  46. // first, check that the status matches
  47. if status := rr.Code; status != c.expStatus {
  48. t.Errorf("%s, handler returned wrong status code: got %v want %v",
  49. c.msg, status, c.expStatus)
  50. }
  51. // if there's a validator, call it
  52. for _, validate := range c.validators {
  53. validate(c, tester, t)
  54. }
  55. }
  56. }
  57. // ------------------------- TEST FIXTURES AND FUNCTIONS ------------------------- //
  58. var createProjectTests = []*projTest{
  59. &projTest{
  60. initializers: []func(t *tester){
  61. initUserDefault,
  62. },
  63. msg: "Create project",
  64. method: "POST",
  65. endpoint: "/api/projects",
  66. body: `{
  67. "name": "project-test"
  68. }`,
  69. expStatus: http.StatusCreated,
  70. expBody: `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"admin","user_id":1,"project_id":1}]}`,
  71. useCookie: true,
  72. validators: []func(c *projTest, tester *tester, t *testing.T){
  73. projectModelBodyValidator,
  74. },
  75. },
  76. }
  77. func TestHandleCreateProject(t *testing.T) {
  78. testProjRequests(t, createProjectTests, true)
  79. }
  80. var readProjectTests = []*projTest{
  81. &projTest{
  82. initializers: []func(t *tester){
  83. initUserDefault,
  84. initProject,
  85. },
  86. msg: "Read project",
  87. method: "GET",
  88. endpoint: "/api/projects/1",
  89. body: ``,
  90. expStatus: http.StatusOK,
  91. expBody: `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"admin","user_id":1,"project_id":1}]}`,
  92. useCookie: true,
  93. validators: []func(c *projTest, tester *tester, t *testing.T){
  94. projectModelBodyValidator,
  95. },
  96. },
  97. }
  98. func TestHandleReadProject(t *testing.T) {
  99. testProjRequests(t, readProjectTests, true)
  100. }
  101. var readProjectSATest = []*projTest{
  102. &projTest{
  103. initializers: []func(t *tester){
  104. initUserDefault,
  105. initProject,
  106. initProjectSADefault,
  107. },
  108. msg: "Read project service account",
  109. method: "GET",
  110. endpoint: "/api/projects/1/serviceAccounts/1",
  111. body: ``,
  112. expStatus: http.StatusOK,
  113. expBody: `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}],"auth_mechanism":"oidc"}`,
  114. useCookie: true,
  115. validators: []func(c *projTest, tester *tester, t *testing.T){
  116. projectSABodyValidator,
  117. },
  118. },
  119. }
  120. func TestHandleReadProjectSA(t *testing.T) {
  121. testProjRequests(t, readProjectSATest, true)
  122. }
  123. var listProjectClustersTest = []*projTest{
  124. &projTest{
  125. initializers: []func(t *tester){
  126. initUserDefault,
  127. initProject,
  128. initProjectSADefault,
  129. },
  130. msg: "List project clusters",
  131. method: "GET",
  132. endpoint: "/api/projects/1/clusters",
  133. body: ``,
  134. expStatus: http.StatusOK,
  135. expBody: `[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}]`,
  136. useCookie: true,
  137. validators: []func(c *projTest, tester *tester, t *testing.T){
  138. projectClustersValidator,
  139. },
  140. },
  141. }
  142. func TestHandleListProjectClusters(t *testing.T) {
  143. testProjRequests(t, listProjectClustersTest, true)
  144. }
  145. var createProjectSACandidatesTests = []*projTest{
  146. &projTest{
  147. initializers: []func(t *tester){
  148. initUserDefault,
  149. initProject,
  150. },
  151. msg: "Create project SA candidate w/ no actions -- should create SA by default",
  152. method: "POST",
  153. endpoint: "/api/projects/1/candidates",
  154. body: `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
  155. expStatus: http.StatusCreated,
  156. expBody: `[{"id":1,"actions":[],"created_sa_id":1,"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
  157. useCookie: true,
  158. validators: []func(c *projTest, tester *tester, t *testing.T){
  159. projectSACandidateBodyValidator,
  160. // check that ServiceAccount was created by default
  161. func(c *projTest, tester *tester, t *testing.T) {
  162. serviceAccounts, err := tester.repo.ServiceAccount.ListServiceAccountsByProjectID(1)
  163. if err != nil {
  164. t.Fatalf("%v\n", err)
  165. }
  166. if len(serviceAccounts) != 1 {
  167. t.Fatal("Expected service account to be created by default, but does not exist\n")
  168. }
  169. sa := serviceAccounts[0]
  170. if len(sa.Clusters) != 1 {
  171. t.Fatalf("cluster not written\n")
  172. }
  173. if sa.Clusters[0].ServiceAccountID != 1 {
  174. t.Errorf("service account ID of joined cluster is not 1")
  175. }
  176. if sa.AuthMechanism != models.OIDC {
  177. t.Errorf("service account auth mechanism is not %s\n", models.OIDC)
  178. }
  179. if string(sa.OIDCCertificateAuthorityData) != "LS0tLS1CRUdJTiBDRVJ=" {
  180. t.Errorf("service account key data and input do not match: expected %s, got %s\n",
  181. string(sa.OIDCCertificateAuthorityData), "LS0tLS1CRUdJTiBDRVJ=")
  182. }
  183. if string(sa.OIDCClientID) != "porter-api" {
  184. t.Errorf("service account oidc client id is not %s\n", "porter-api")
  185. }
  186. if string(sa.OIDCIDToken) != "token" {
  187. t.Errorf("service account oidc id token is not %s\n", "token")
  188. }
  189. },
  190. },
  191. },
  192. &projTest{
  193. initializers: []func(t *tester){
  194. initUserDefault,
  195. initProject,
  196. },
  197. msg: "Create project SA candidate",
  198. method: "POST",
  199. endpoint: "/api/projects/1/candidates",
  200. body: `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
  201. expStatus: http.StatusCreated,
  202. expBody: `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
  203. useCookie: true,
  204. validators: []func(c *projTest, tester *tester, t *testing.T){
  205. projectSACandidateBodyValidator,
  206. },
  207. },
  208. }
  209. func TestHandleCreateProjectSACandidate(t *testing.T) {
  210. testProjRequests(t, createProjectSACandidatesTests, true)
  211. }
  212. var listProjectSACandidatesTests = []*projTest{
  213. &projTest{
  214. initializers: []func(t *tester){
  215. initUserDefault,
  216. initProject,
  217. initProjectSACandidate,
  218. },
  219. msg: "List project SA candidates",
  220. method: "GET",
  221. endpoint: "/api/projects/1/candidates",
  222. body: ``,
  223. expStatus: http.StatusOK,
  224. expBody: `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
  225. useCookie: true,
  226. validators: []func(c *projTest, tester *tester, t *testing.T){
  227. projectSACandidateBodyValidator,
  228. },
  229. },
  230. }
  231. func TestHandleListProjectSACandidates(t *testing.T) {
  232. testProjRequests(t, listProjectSACandidatesTests, true)
  233. }
  234. var resolveProjectSACandidatesTests = []*projTest{
  235. &projTest{
  236. initializers: []func(t *tester){
  237. initUserDefault,
  238. initProject,
  239. initProjectSACandidate,
  240. },
  241. msg: "Resolve project SA candidate",
  242. method: "POST",
  243. endpoint: "/api/projects/1/candidates/1/resolve",
  244. body: `[{"name": "upload-oidc-idp-issuer-ca-data", "oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}]`,
  245. expStatus: http.StatusCreated,
  246. expBody: `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}],"auth_mechanism":"oidc"}`,
  247. useCookie: true,
  248. validators: []func(c *projTest, tester *tester, t *testing.T){
  249. projectSABodyValidator,
  250. },
  251. },
  252. }
  253. func TestHandleResolveProjectSACandidate(t *testing.T) {
  254. testProjRequests(t, resolveProjectSACandidatesTests, true)
  255. }
  256. var deleteProjectTests = []*projTest{
  257. &projTest{
  258. initializers: []func(t *tester){
  259. initUserDefault,
  260. initProject,
  261. },
  262. msg: "Delete project",
  263. method: "DELETE",
  264. endpoint: "/api/projects/1",
  265. body: ``,
  266. expStatus: http.StatusOK,
  267. expBody: `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"admin","user_id":1,"project_id":1}]}`,
  268. useCookie: true,
  269. validators: []func(c *projTest, tester *tester, t *testing.T){
  270. projectModelBodyValidator,
  271. },
  272. },
  273. }
  274. func TestHandleDeleteProject(t *testing.T) {
  275. testProjRequests(t, deleteProjectTests, true)
  276. }
  277. // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
  278. func initProject(tester *tester) {
  279. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  280. // handle write to the database
  281. projModel, _ := tester.repo.Project.CreateProject(&models.Project{
  282. Name: "project-test",
  283. })
  284. // create a new Role with the user as the owner
  285. tester.repo.Project.CreateProjectRole(projModel, &models.Role{
  286. UserID: user.ID,
  287. ProjectID: projModel.ID,
  288. Kind: models.RoleAdmin,
  289. })
  290. }
  291. func initProjectSACandidate(tester *tester) {
  292. proj, _ := tester.repo.Project.ReadProject(1)
  293. form := &forms.CreateServiceAccountCandidatesForm{
  294. ProjectID: uint(proj.ID),
  295. Kubeconfig: OIDCAuthWithoutData,
  296. }
  297. // convert the form to a ServiceAccountCandidate
  298. saCandidates, _ := form.ToServiceAccountCandidates()
  299. for _, saCandidate := range saCandidates {
  300. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  301. }
  302. }
  303. func initProjectSADefault(tester *tester) {
  304. proj, _ := tester.repo.Project.ReadProject(1)
  305. form := &forms.CreateServiceAccountCandidatesForm{
  306. ProjectID: uint(proj.ID),
  307. Kubeconfig: OIDCAuthWithData,
  308. }
  309. // convert the form to a ServiceAccountCandidate
  310. saCandidates, _ := form.ToServiceAccountCandidates()
  311. for _, saCandidate := range saCandidates {
  312. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  313. }
  314. saForm := forms.ServiceAccountActionResolver{
  315. ServiceAccountCandidateID: 1,
  316. }
  317. saForm.PopulateServiceAccount(tester.repo.ServiceAccount)
  318. tester.repo.ServiceAccount.CreateServiceAccount(saForm.SA)
  319. }
  320. func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
  321. if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
  322. t.Errorf("%s, handler returned wrong body: got %v want %v",
  323. c.msg, body, c.expBody)
  324. }
  325. }
  326. func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
  327. gotBody := &models.ProjectExternal{}
  328. expBody := &models.ProjectExternal{}
  329. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  330. json.Unmarshal([]byte(c.expBody), expBody)
  331. if !reflect.DeepEqual(gotBody, expBody) {
  332. t.Errorf("%s, handler returned wrong body: got %v want %v",
  333. c.msg, gotBody, expBody)
  334. }
  335. }
  336. func projectSACandidateBodyValidator(c *projTest, tester *tester, t *testing.T) {
  337. gotBody := make([]*models.ServiceAccountCandidateExternal, 0)
  338. expBody := make([]*models.ServiceAccountCandidateExternal, 0)
  339. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  340. json.Unmarshal([]byte(c.expBody), &expBody)
  341. if !reflect.DeepEqual(gotBody, expBody) {
  342. t.Errorf("%s, handler returned wrong body: got %v want %v",
  343. c.msg, gotBody, expBody)
  344. }
  345. }
  346. func projectSABodyValidator(c *projTest, tester *tester, t *testing.T) {
  347. gotBody := &models.ServiceAccountExternal{}
  348. expBody := &models.ServiceAccountExternal{}
  349. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  350. json.Unmarshal([]byte(c.expBody), expBody)
  351. if !reflect.DeepEqual(gotBody, expBody) {
  352. t.Errorf("%s, handler returned wrong body: got %v want %v",
  353. c.msg, gotBody, expBody)
  354. }
  355. }
  356. func projectClustersValidator(c *projTest, tester *tester, t *testing.T) {
  357. gotBody := make([]*models.ClusterExternal, 0)
  358. expBody := make([]*models.ClusterExternal, 0)
  359. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  360. json.Unmarshal([]byte(c.expBody), &expBody)
  361. if diff := deep.Equal(gotBody, expBody); diff != nil {
  362. t.Errorf("handler returned wrong body:\n")
  363. t.Error(diff)
  364. }
  365. }
  366. const OIDCAuthWithDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n server: https://localhost\n certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n user:\n auth-provider:\n config:\n client-id: porter-api\n id-token: token\n idp-issuer-url: https://localhost\n idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n name: oidc`
  367. const OIDCAuthWithoutDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n server: https://localhost\n certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n user:\n auth-provider:\n config:\n client-id: porter-api\n id-token: token\n idp-issuer-url: https://localhost\n idp-certificate-authority: /fake/path/to/ca.pem\n name: oidc`
  368. const OIDCAuthWithoutData string = `
  369. apiVersion: v1
  370. clusters:
  371. - cluster:
  372. server: https://localhost
  373. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  374. name: cluster-test
  375. contexts:
  376. - context:
  377. cluster: cluster-test
  378. user: test-admin
  379. name: context-test
  380. current-context: context-test
  381. kind: Config
  382. preferences: {}
  383. users:
  384. - name: test-admin
  385. user:
  386. auth-provider:
  387. config:
  388. client-id: porter-api
  389. id-token: token
  390. idp-issuer-url: https://localhost
  391. idp-certificate-authority: /fake/path/to/ca.pem
  392. name: oidc
  393. `
  394. const OIDCAuthWithData string = `
  395. apiVersion: v1
  396. clusters:
  397. - cluster:
  398. server: https://localhost
  399. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  400. name: cluster-test
  401. contexts:
  402. - context:
  403. cluster: cluster-test
  404. user: test-admin
  405. name: context-test
  406. current-context: context-test
  407. kind: Config
  408. preferences: {}
  409. users:
  410. - name: test-admin
  411. user:
  412. auth-provider:
  413. config:
  414. client-id: porter-api
  415. id-token: token
  416. idp-issuer-url: https://localhost
  417. idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  418. name: oidc
  419. `