project_handler_test.go 12 KB

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