project_handler_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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. // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
  235. func initProject(tester *tester) {
  236. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  237. // handle write to the database
  238. projModel, _ := tester.repo.Project.CreateProject(&models.Project{
  239. Name: "project-test",
  240. })
  241. // create a new Role with the user as the owner
  242. tester.repo.Project.CreateProjectRole(projModel, &models.Role{
  243. UserID: user.ID,
  244. ProjectID: projModel.ID,
  245. Kind: models.RoleAdmin,
  246. })
  247. }
  248. func initProjectSACandidate(tester *tester) {
  249. proj, _ := tester.repo.Project.ReadProject(1)
  250. form := &forms.CreateServiceAccountCandidatesForm{
  251. ProjectID: uint(proj.ID),
  252. Kubeconfig: OIDCAuthWithoutData,
  253. }
  254. // convert the form to a ServiceAccountCandidate
  255. saCandidates, _ := form.ToServiceAccountCandidates()
  256. for _, saCandidate := range saCandidates {
  257. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  258. }
  259. }
  260. func initProjectSADefault(tester *tester) {
  261. proj, _ := tester.repo.Project.ReadProject(1)
  262. form := &forms.CreateServiceAccountCandidatesForm{
  263. ProjectID: uint(proj.ID),
  264. Kubeconfig: OIDCAuthWithData,
  265. }
  266. // convert the form to a ServiceAccountCandidate
  267. saCandidates, _ := form.ToServiceAccountCandidates()
  268. for _, saCandidate := range saCandidates {
  269. tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
  270. }
  271. saForm := forms.ServiceAccountActionResolver{
  272. ServiceAccountCandidateID: 1,
  273. }
  274. saForm.PopulateServiceAccount(tester.repo.ServiceAccount)
  275. tester.repo.ServiceAccount.CreateServiceAccount(saForm.SA)
  276. }
  277. func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
  278. if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
  279. t.Errorf("%s, handler returned wrong body: got %v want %v",
  280. c.msg, body, c.expBody)
  281. }
  282. }
  283. func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
  284. gotBody := &models.ProjectExternal{}
  285. expBody := &models.ProjectExternal{}
  286. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  287. json.Unmarshal([]byte(c.expBody), expBody)
  288. if !reflect.DeepEqual(gotBody, expBody) {
  289. t.Errorf("%s, handler returned wrong body: got %v want %v",
  290. c.msg, gotBody, expBody)
  291. }
  292. }
  293. func projectSACandidateBodyValidator(c *projTest, tester *tester, t *testing.T) {
  294. gotBody := make([]*models.ServiceAccountCandidateExternal, 0)
  295. expBody := make([]*models.ServiceAccountCandidateExternal, 0)
  296. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  297. json.Unmarshal([]byte(c.expBody), &expBody)
  298. if !reflect.DeepEqual(gotBody, expBody) {
  299. t.Errorf("%s, handler returned wrong body: got %v want %v",
  300. c.msg, gotBody, expBody)
  301. }
  302. }
  303. func projectSABodyValidator(c *projTest, tester *tester, t *testing.T) {
  304. gotBody := &models.ServiceAccountExternal{}
  305. expBody := &models.ServiceAccountExternal{}
  306. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  307. json.Unmarshal([]byte(c.expBody), expBody)
  308. if !reflect.DeepEqual(gotBody, expBody) {
  309. t.Errorf("%s, handler returned wrong body: got %v want %v",
  310. c.msg, gotBody, expBody)
  311. }
  312. }
  313. func projectClustersValidator(c *projTest, tester *tester, t *testing.T) {
  314. gotBody := make([]*models.ClusterExternal, 0)
  315. expBody := make([]*models.ClusterExternal, 0)
  316. json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
  317. json.Unmarshal([]byte(c.expBody), &expBody)
  318. if diff := deep.Equal(gotBody, expBody); diff != nil {
  319. t.Errorf("handler returned wrong body:\n")
  320. t.Error(diff)
  321. }
  322. }
  323. 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`
  324. 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`
  325. const OIDCAuthWithoutData string = `
  326. apiVersion: v1
  327. clusters:
  328. - cluster:
  329. server: https://localhost
  330. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  331. name: cluster-test
  332. contexts:
  333. - context:
  334. cluster: cluster-test
  335. user: test-admin
  336. name: context-test
  337. current-context: context-test
  338. kind: Config
  339. preferences: {}
  340. users:
  341. - name: test-admin
  342. user:
  343. auth-provider:
  344. config:
  345. client-id: porter-api
  346. id-token: token
  347. idp-issuer-url: https://localhost
  348. idp-certificate-authority: /fake/path/to/ca.pem
  349. name: oidc
  350. `
  351. const OIDCAuthWithData string = `
  352. apiVersion: v1
  353. clusters:
  354. - cluster:
  355. server: https://localhost
  356. certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  357. name: cluster-test
  358. contexts:
  359. - context:
  360. cluster: cluster-test
  361. user: test-admin
  362. name: context-test
  363. current-context: context-test
  364. kind: Config
  365. preferences: {}
  366. users:
  367. - name: test-admin
  368. user:
  369. auth-provider:
  370. config:
  371. client-id: porter-api
  372. id-token: token
  373. idp-issuer-url: https://localhost
  374. idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
  375. name: oidc
  376. `