project_handler_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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 listProjectClustersTest = []*projTest{
  102. &projTest{
  103. initializers: []func(t *tester){
  104. initUserDefault,
  105. initProject,
  106. initProjectSADefault,
  107. },
  108. msg: "List project clusters",
  109. method: "GET",
  110. endpoint: "/api/projects/1/clusters",
  111. body: ``,
  112. expStatus: http.StatusOK,
  113. expBody: `[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}]`,
  114. useCookie: true,
  115. validators: []func(c *projTest, tester *tester, t *testing.T){
  116. projectClustersValidator,
  117. },
  118. },
  119. }
  120. func TestHandleListProjectClusters(t *testing.T) {
  121. testProjRequests(t, listProjectClustersTest, true)
  122. }
  123. var createProjectSACandidatesTests = []*projTest{
  124. &projTest{
  125. initializers: []func(t *tester){
  126. initUserDefault,
  127. initProject,
  128. },
  129. msg: "Create project SA candidate w/ no actions -- should create SA by default",
  130. method: "POST",
  131. endpoint: "/api/projects/1/candidates",
  132. body: `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
  133. expStatus: http.StatusCreated,
  134. expBody: `[{"id":1,"actions":[],"project_id":1,"kind":"connector","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
  135. useCookie: true,
  136. validators: []func(c *projTest, tester *tester, t *testing.T){
  137. projectSACandidateBodyValidator,
  138. // check that ServiceAccount was created by default
  139. func(c *projTest, tester *tester, t *testing.T) {
  140. serviceAccounts, err := tester.repo.ServiceAccount.ListServiceAccountsByProjectID(1)
  141. if err != nil {
  142. t.Fatalf("%v\n", err)
  143. }
  144. if len(serviceAccounts) != 1 {
  145. t.Fatal("Expected service account to be created by default, but does not exist\n")
  146. }
  147. sa := serviceAccounts[0]
  148. if len(sa.Clusters) != 1 {
  149. t.Fatalf("cluster not written\n")
  150. }
  151. if sa.Clusters[0].ServiceAccountID != 1 {
  152. t.Errorf("service account ID of joined cluster is not 1")
  153. }
  154. if sa.AuthMechanism != models.OIDC {
  155. t.Errorf("service account auth mechanism is not %s\n", models.OIDC)
  156. }
  157. if string(sa.OIDCCertificateAuthorityData) != "LS0tLS1CRUdJTiBDRVJ=" {
  158. t.Errorf("service account key data and input do not match: expected %s, got %s\n",
  159. string(sa.OIDCCertificateAuthorityData), "LS0tLS1CRUdJTiBDRVJ=")
  160. }
  161. if sa.OIDCClientID != "porter-api" {
  162. t.Errorf("service account oidc client id is not %s\n", "porter-api")
  163. }
  164. if sa.OIDCIDToken != "token" {
  165. t.Errorf("service account oidc id token is not %s\n", "token")
  166. }
  167. },
  168. },
  169. },
  170. &projTest{
  171. initializers: []func(t *tester){
  172. initUserDefault,
  173. initProject,
  174. },
  175. msg: "Create project SA candidate",
  176. method: "POST",
  177. endpoint: "/api/projects/1/candidates",
  178. body: `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
  179. expStatus: http.StatusCreated,
  180. 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","cluster_name":"cluster-test","cluster_endpoint":"https://localhost","auth_mechanism":"oidc"}]`,
  181. useCookie: true,
  182. validators: []func(c *projTest, tester *tester, t *testing.T){
  183. projectSACandidateBodyValidator,
  184. },
  185. },
  186. }
  187. func TestHandleCreateProjectSACandidate(t *testing.T) {
  188. testProjRequests(t, createProjectSACandidatesTests, true)
  189. }
  190. var listProjectSACandidatesTests = []*projTest{
  191. &projTest{
  192. initializers: []func(t *tester){
  193. initUserDefault,
  194. initProject,
  195. initProjectSACandidate,
  196. },
  197. msg: "List project SA candidates",
  198. method: "GET",
  199. endpoint: "/api/projects/1/candidates",
  200. body: ``,
  201. expStatus: http.StatusOK,
  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","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 TestHandleListProjectSACandidates(t *testing.T) {
  210. testProjRequests(t, listProjectSACandidatesTests, true)
  211. }
  212. var resolveProjectSACandidatesTests = []*projTest{
  213. &projTest{
  214. initializers: []func(t *tester){
  215. initUserDefault,
  216. initProject,
  217. initProjectSACandidate,
  218. },
  219. msg: "Resolve project SA candidate",
  220. method: "POST",
  221. endpoint: "/api/projects/1/candidates/1/resolve",
  222. body: `[{"name": "upload-oidc-idp-issuer-ca-data", "oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}]`,
  223. expStatus: http.StatusCreated,
  224. expBody: `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://localhost"}],"auth_mechanism":"oidc"}`,
  225. useCookie: true,
  226. validators: []func(c *projTest, tester *tester, t *testing.T){
  227. projectSABodyValidator,
  228. },
  229. },
  230. }
  231. func TestHandleResolveProjectSACandidate(t *testing.T) {
  232. testProjRequests(t, resolveProjectSACandidatesTests, true)
  233. }
  234. var deleteProjectTests = []*projTest{
  235. &projTest{
  236. initializers: []func(t *tester){
  237. initUserDefault,
  238. initProject,
  239. },
  240. msg: "Delete project",
  241. method: "DELETE",
  242. endpoint: "/api/projects/1",
  243. body: ``,
  244. expStatus: http.StatusOK,
  245. expBody: `{"id":1,"name":"project-test","roles":[{"id":0,"kind":"admin","user_id":1,"project_id":1}]}`,
  246. useCookie: true,
  247. validators: []func(c *projTest, tester *tester, t *testing.T){
  248. projectModelBodyValidator,
  249. },
  250. },
  251. }
  252. func TestHandleDeleteProject(t *testing.T) {
  253. testProjRequests(t, deleteProjectTests, true)
  254. }
  255. // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
  256. func initProject(tester *tester) {
  257. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  258. // handle write to the database
  259. projModel, _ := tester.repo.Project.CreateProject(&models.Project{
  260. Name: "project-test",
  261. })
  262. // create a new Role with the user as the owner
  263. tester.repo.Project.CreateProjectRole(projModel, &models.Role{
  264. UserID: user.ID,
  265. ProjectID: projModel.ID,
  266. Kind: models.RoleAdmin,
  267. })
  268. }
  269. func initProjectSACandidate(tester *tester) {
  270. proj, _ := tester.repo.Project.ReadProject(1)
  271. form := &forms.CreateServiceAccountCandidatesForm{
  272. ProjectID: uint(proj.ID),
  273. Kubeconfig: OIDCAuthWithoutData,
  274. }
  275. // convert the form to a ServiceAccountCandidate
  276. saCandidates, _ := form.ToServiceAccountCandidates()
  277. for _, saCandidate := range saCandidates {
  278. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  279. }
  280. }
  281. func initProjectSADefault(tester *tester) {
  282. proj, _ := tester.repo.Project.ReadProject(1)
  283. form := &forms.CreateServiceAccountCandidatesForm{
  284. ProjectID: uint(proj.ID),
  285. Kubeconfig: OIDCAuthWithData,
  286. }
  287. // convert the form to a ServiceAccountCandidate
  288. saCandidates, _ := form.ToServiceAccountCandidates()
  289. for _, saCandidate := range saCandidates {
  290. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  291. }
  292. saForm := forms.ServiceAccountActionResolver{
  293. ServiceAccountCandidateID: 1,
  294. }
  295. saForm.PopulateServiceAccount(tester.repo.ServiceAccount)
  296. tester.repo.ServiceAccount.CreateServiceAccount(saForm.SA)
  297. }
  298. func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
  299. if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
  300. t.Errorf("%s, handler returned wrong body: got %v want %v",
  301. c.msg, body, c.expBody)
  302. }
  303. }
  304. func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
  305. gotBody := &models.ProjectExternal{}
  306. expBody := &models.ProjectExternal{}
  307. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  308. json.Unmarshal([]byte(c.expBody), expBody)
  309. if !reflect.DeepEqual(gotBody, expBody) {
  310. t.Errorf("%s, handler returned wrong body: got %v want %v",
  311. c.msg, gotBody, expBody)
  312. }
  313. }
  314. func projectSACandidateBodyValidator(c *projTest, tester *tester, t *testing.T) {
  315. gotBody := make([]*models.ServiceAccountCandidateExternal, 0)
  316. expBody := make([]*models.ServiceAccountCandidateExternal, 0)
  317. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  318. json.Unmarshal([]byte(c.expBody), &expBody)
  319. if !reflect.DeepEqual(gotBody, expBody) {
  320. t.Errorf("%s, handler returned wrong body: got %v want %v",
  321. c.msg, gotBody, expBody)
  322. }
  323. }
  324. func projectSABodyValidator(c *projTest, tester *tester, t *testing.T) {
  325. gotBody := &models.ServiceAccountExternal{}
  326. expBody := &models.ServiceAccountExternal{}
  327. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  328. json.Unmarshal([]byte(c.expBody), expBody)
  329. if !reflect.DeepEqual(gotBody, expBody) {
  330. t.Errorf("%s, handler returned wrong body: got %v want %v",
  331. c.msg, gotBody, expBody)
  332. }
  333. }
  334. func projectClustersValidator(c *projTest, tester *tester, t *testing.T) {
  335. gotBody := make([]*models.ClusterExternal, 0)
  336. expBody := make([]*models.ClusterExternal, 0)
  337. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  338. json.Unmarshal([]byte(c.expBody), &expBody)
  339. if diff := deep.Equal(gotBody, expBody); diff != nil {
  340. t.Errorf("handler returned wrong body:\n")
  341. t.Error(diff)
  342. }
  343. }
  344. 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`
  345. 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`
  346. const OIDCAuthWithoutData string = `
  347. apiVersion: v1
  348. clusters:
  349. - cluster:
  350. server: https://localhost
  351. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  352. name: cluster-test
  353. contexts:
  354. - context:
  355. cluster: cluster-test
  356. user: test-admin
  357. name: context-test
  358. current-context: context-test
  359. kind: Config
  360. preferences: {}
  361. users:
  362. - name: test-admin
  363. user:
  364. auth-provider:
  365. config:
  366. client-id: porter-api
  367. id-token: token
  368. idp-issuer-url: https://localhost
  369. idp-certificate-authority: /fake/path/to/ca.pem
  370. name: oidc
  371. `
  372. const OIDCAuthWithData string = `
  373. apiVersion: v1
  374. clusters:
  375. - cluster:
  376. server: https://localhost
  377. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  378. name: cluster-test
  379. contexts:
  380. - context:
  381. cluster: cluster-test
  382. user: test-admin
  383. name: context-test
  384. current-context: context-test
  385. kind: Config
  386. preferences: {}
  387. users:
  388. - name: test-admin
  389. user:
  390. auth-provider:
  391. config:
  392. client-id: porter-api
  393. id-token: token
  394. idp-issuer-url: https://localhost
  395. idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  396. name: oidc
  397. `