From f603c1e4090fa46c396fa0167b6a205786fcc7cb Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 19:30:04 +0100 Subject: [PATCH] adding course_phase tests --- server/coursePhase/router.go | 2 +- server/coursePhase/router_test.go | 150 ++++++++++++++++++++ server/coursePhase/service_test.go | 107 ++++++++++++++ server/coursePhase/validation_test.go | 136 ++++++++++++++++++ server/database_dumps/course_phase_test.sql | 67 +++++++++ 5 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 server/coursePhase/router_test.go create mode 100644 server/coursePhase/service_test.go create mode 100644 server/coursePhase/validation_test.go create mode 100644 server/database_dumps/course_phase_test.sql diff --git a/server/coursePhase/router.go b/server/coursePhase/router.go index 154b646..8e319a9 100644 --- a/server/coursePhase/router.go +++ b/server/coursePhase/router.go @@ -11,7 +11,7 @@ import ( func setupCoursePhaseRouter(router *gin.RouterGroup) { coursePhase := router.Group("/course_phases") coursePhase.GET("/:uuid", getCoursePhaseByID) - coursePhase.POST("/", createCoursePhase) + coursePhase.POST("", createCoursePhase) coursePhase.PUT("/:uuid", updateCoursePhase) } diff --git a/server/coursePhase/router_test.go b/server/coursePhase/router_test.go new file mode 100644 index 0000000..df46505 --- /dev/null +++ b/server/coursePhase/router_test.go @@ -0,0 +1,150 @@ +package coursePhase + +import ( + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseDTO" + "github.com/niclasheun/prompt2.0/meta" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type RouterTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + coursePhaseService CoursePhaseService +} + +func (suite *RouterTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/course_phase_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.coursePhaseService = CoursePhaseService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CoursePhaseServiceSingleton = &suite.coursePhaseService + + suite.router = setupRouter() +} + +func (suite *RouterTestSuite) TearDownSuite() { + suite.cleanup() +} + +func setupRouter() *gin.Engine { + router := gin.Default() + api := router.Group("/api") + setupCoursePhaseRouter(api) + return router +} + +func (suite *RouterTestSuite) TestGetCoursePhaseByID() { + req := httptest.NewRequest(http.MethodGet, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b", nil) + w := httptest.NewRecorder() + + suite.router.ServeHTTP(w, req) + + assert.Equal(suite.T(), http.StatusOK, w.Code) + + var coursePhase coursePhaseDTO.CoursePhase + err := json.Unmarshal(w.Body.Bytes(), &coursePhase) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "Test", coursePhase.Name, "Expected course phase name to match") + assert.False(suite.T(), coursePhase.IsInitialPhase, "Expected course phase to not be an initial phase") + assert.Equal(suite.T(), uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), coursePhase.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744b"), coursePhase.CoursePhaseTypeID, "Expected CoursePhaseTypeID to match") + assert.Equal(suite.T(), "test-value", coursePhase.MetaData["test-key"], "Expected MetaData to match") +} + +func (suite *RouterTestSuite) TestCreateCoursePhase() { + jsonData := `{"new_key": "new_value"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + newCoursePhase := coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), + Name: "New Phase", + IsInitialPhase: false, + MetaData: metaData, + CoursePhaseTypeID: uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744c"), + } + + body, _ := json.Marshal(newCoursePhase) + req := httptest.NewRequest(http.MethodPost, "/api/course_phases", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + suite.router.ServeHTTP(w, req) + + assert.Equal(suite.T(), http.StatusCreated, w.Code) + + var createdCoursePhase coursePhaseDTO.CoursePhase + err = json.Unmarshal(w.Body.Bytes(), &createdCoursePhase) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "New Phase", createdCoursePhase.Name, "Expected course phase name to match") + assert.False(suite.T(), createdCoursePhase.IsInitialPhase, "Expected course phase to not be an initial phase") + assert.Equal(suite.T(), newCoursePhase.CourseID, createdCoursePhase.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), newCoursePhase.MetaData, createdCoursePhase.MetaData, "Expected MetaData to match") + assert.Equal(suite.T(), newCoursePhase.CoursePhaseTypeID, createdCoursePhase.CoursePhaseTypeID, "Expected CoursePhaseTypeID to match") +} + +func (suite *RouterTestSuite) TestUpdateCoursePhase() { + jsonData := `{"updated_key": "updated_value"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + updatedCoursePhase := coursePhaseDTO.UpdateCoursePhase{ + ID: uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b"), + Name: "Updated Phase", + IsInitialPhase: false, + MetaData: metaData, + } + + body, _ := json.Marshal(updatedCoursePhase) + req := httptest.NewRequest(http.MethodPut, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + suite.router.ServeHTTP(w, req) + + assert.Equal(suite.T(), http.StatusOK, w.Code) + + // Verify the update by fetching the updated course phase + fetchReq := httptest.NewRequest(http.MethodGet, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b", nil) + fetchRes := httptest.NewRecorder() + suite.router.ServeHTTP(fetchRes, fetchReq) + + assert.Equal(suite.T(), http.StatusOK, fetchRes.Code) + + var fetchedCoursePhase coursePhaseDTO.CoursePhase + err = json.Unmarshal(fetchRes.Body.Bytes(), &fetchedCoursePhase) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "Updated Phase", fetchedCoursePhase.Name, "Expected updated course phase name to match") + assert.False(suite.T(), fetchedCoursePhase.IsInitialPhase, "Expected course phase not to be an initial phase") + assert.Equal(suite.T(), updatedCoursePhase.MetaData["updated_key"], fetchedCoursePhase.MetaData["updated_key"], "Expected updated metadata to match") + assert.Equal(suite.T(), "test-value", fetchedCoursePhase.MetaData["test-key"], "Expected existing metadata to match") +} + +func TestRouterTestSuite(t *testing.T) { + suite.Run(t, new(RouterTestSuite)) +} diff --git a/server/coursePhase/service_test.go b/server/coursePhase/service_test.go new file mode 100644 index 0000000..e04b2e4 --- /dev/null +++ b/server/coursePhase/service_test.go @@ -0,0 +1,107 @@ +package coursePhase + +import ( + "context" + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseDTO" + "github.com/niclasheun/prompt2.0/meta" + "github.com/niclasheun/prompt2.0/testutils" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CoursePhaseTestSuite struct { + suite.Suite + ctx context.Context + cleanup func() + coursePhaseService CoursePhaseService +} + +func (suite *CoursePhaseTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/course_phase_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.coursePhaseService = CoursePhaseService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CoursePhaseServiceSingleton = &suite.coursePhaseService +} + +func (suite *CoursePhaseTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CoursePhaseTestSuite) TestGetCoursePhaseByID() { + id := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + coursePhase, err := GetCoursePhaseByID(suite.ctx, id) + + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "Test", coursePhase.Name, "Expected course phase name to match") + assert.False(suite.T(), coursePhase.IsInitialPhase, "Expected course phase to not be an initial phase") + assert.Equal(suite.T(), id, coursePhase.ID, "Expected course phase ID to match") +} + +func (suite *CoursePhaseTestSuite) TestUpdateCoursePhase() { + id := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + jsonData := `{"updated_key": "updated_value"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + update := coursePhaseDTO.UpdateCoursePhase{ + ID: id, + Name: "Updated Phase", + IsInitialPhase: false, + MetaData: metaData, + } + + err = UpdateCoursePhase(suite.ctx, update) + assert.NoError(suite.T(), err) + + // Verify update + updatedCoursePhase, err := GetCoursePhaseByID(suite.ctx, id) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "Updated Phase", updatedCoursePhase.Name, "Expected updated course phase name to match") + assert.False(suite.T(), updatedCoursePhase.IsInitialPhase, "Expected updated course phase to be an initial phase") + assert.Equal(suite.T(), metaData, updatedCoursePhase.MetaData, "Expected metadata to match updated data") +} + +func (suite *CoursePhaseTestSuite) TestCreateCoursePhase() { + jsonData := `{"new_key": "new_value"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + newCoursePhase := coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), + Name: "New Phase", + IsInitialPhase: false, + MetaData: metaData, + CoursePhaseTypeID: uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744c"), + } + + createdCoursePhase, err := CreateCoursePhase(suite.ctx, newCoursePhase) + assert.NoError(suite.T(), err) + + // Verify creation + fetchedCoursePhase, err := GetCoursePhaseByID(suite.ctx, createdCoursePhase.ID) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "New Phase", fetchedCoursePhase.Name, "Expected course phase name to match") + assert.False(suite.T(), fetchedCoursePhase.IsInitialPhase, "Expected course phase to not be an initial phase") + assert.Equal(suite.T(), metaData, fetchedCoursePhase.MetaData, "Expected metadata to match") +} + +func TestCoursePhaseTestSuite(t *testing.T) { + suite.Run(t, new(CoursePhaseTestSuite)) +} diff --git a/server/coursePhase/validation_test.go b/server/coursePhase/validation_test.go new file mode 100644 index 0000000..7e5a5eb --- /dev/null +++ b/server/coursePhase/validation_test.go @@ -0,0 +1,136 @@ +package coursePhase + +import ( + "testing" + + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseDTO" + "github.com/niclasheun/prompt2.0/meta" + "github.com/stretchr/testify/assert" +) + +func TestValidateCreateCoursePhase(t *testing.T) { + tests := []struct { + name string + input coursePhaseDTO.CreateCoursePhase + expectedError string + }{ + { + name: "valid course phase", + input: coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.New(), + Name: "Phase 1", + IsInitialPhase: true, + MetaData: meta.MetaData{"key": "value"}, + CoursePhaseTypeID: uuid.New(), + }, + expectedError: "", + }, + { + name: "missing name", + input: coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.New(), + Name: "", + IsInitialPhase: false, + MetaData: meta.MetaData{"key": "value"}, + CoursePhaseTypeID: uuid.New(), + }, + expectedError: "course phase name is required", + }, + { + name: "missing course ID", + input: coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.Nil, + Name: "Phase 1", + IsInitialPhase: true, + MetaData: meta.MetaData{"key": "value"}, + CoursePhaseTypeID: uuid.New(), + }, + expectedError: "course id is required", + }, + { + name: "missing name and course ID", + input: coursePhaseDTO.CreateCoursePhase{ + CourseID: uuid.Nil, + Name: "", + IsInitialPhase: false, + MetaData: meta.MetaData{}, + CoursePhaseTypeID: uuid.New(), + }, + expectedError: "course phase name is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateCreateCoursePhase(tt.input) + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, err, tt.expectedError) + } + }) + } +} + +func TestValidateUpdateCoursePhase(t *testing.T) { + tests := []struct { + name string + input coursePhaseDTO.UpdateCoursePhase + expectedError string + }{ + { + name: "valid update", + input: coursePhaseDTO.UpdateCoursePhase{ + ID: uuid.New(), + Name: "Updated Phase Name", + IsInitialPhase: false, + MetaData: meta.MetaData{"key": "value"}, + }, + expectedError: "", + }, + { + name: "missing name", + input: coursePhaseDTO.UpdateCoursePhase{ + ID: uuid.New(), + Name: "", + IsInitialPhase: true, + MetaData: meta.MetaData{"key": "value"}, + }, + expectedError: "course phase name is required", + }, + { + name: "empty metadata", + input: coursePhaseDTO.UpdateCoursePhase{ + ID: uuid.New(), + Name: "Phase with Empty Metadata", + IsInitialPhase: false, + MetaData: meta.MetaData{}, + }, + expectedError: "", + }, + { + name: "missing ID", + input: coursePhaseDTO.UpdateCoursePhase{ + ID: uuid.Nil, + Name: "Valid Name", + IsInitialPhase: false, + MetaData: meta.MetaData{"key": "value"}, + }, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateUpdateCoursePhase(tt.input) + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, err, tt.expectedError) + } + }) + } +} diff --git a/server/database_dumps/course_phase_test.sql b/server/database_dumps/course_phase_test.sql new file mode 100644 index 0000000..a479394 --- /dev/null +++ b/server/database_dumps/course_phase_test.sql @@ -0,0 +1,67 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 15.2 +-- Dumped by pg_dump version 15.8 (Homebrew) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', 'public', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: course_phase; Type: TABLE; Schema: public; Owner: prompt-postgres +-- + +CREATE TABLE course_phase_type ( + id uuid NOT NULL, + name text NOT NULL +); + + +CREATE TABLE course_phase ( + id uuid NOT NULL, + course_id uuid NOT NULL, + name text, + meta_data jsonb, + is_initial_phase boolean NOT NULL, + course_phase_type_id uuid NOT NULL +); + +INSERT INTO course_phase_type (id, name) VALUES ('7dc1c4e8-4255-4874-80a0-0c12b958744b', 'application'); +INSERT INTO course_phase_type (id, name) VALUES ('7dc1c4e8-4255-4874-80a0-0c12b958744c', 'template_component'); + + +-- +-- Data for Name: course_phase; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course_phase (id, course_id, name, meta_data, is_initial_phase, course_phase_type_id) VALUES ('3d1f3b00-87f3-433b-a713-178c4050411b', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', 'Test', '{"test-key":"test-value"}', false, '7dc1c4e8-4255-4874-80a0-0c12b958744b'); +INSERT INTO course_phase (id, course_id, name, meta_data, is_initial_phase, course_phase_type_id) VALUES ('92bb0532-39e5-453d-bc50-fa61ea0128b2', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', 'Template Phase', '{}', false, '7dc1c4e8-4255-4874-80a0-0c12b958744c'); +INSERT INTO course_phase (id, course_id, name, meta_data, is_initial_phase, course_phase_type_id) VALUES ('500db7ed-2eb2-42d0-82b3-8750e12afa8a', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', 'Application Phase', '{}', true, '7dc1c4e8-4255-4874-80a0-0c12b958744b'); + +ALTER TABLE ONLY course_phase + ADD CONSTRAINT course_phase_pkey PRIMARY KEY (id); + +CREATE UNIQUE INDEX unique_initial_phase_per_course ON course_phase USING btree (course_id) WHERE (is_initial_phase = true); + +ALTER TABLE ONLY course_phase_type + ADD CONSTRAINT course_phase_type_name_key UNIQUE (name); + +ALTER TABLE ONLY course_phase_type + ADD CONSTRAINT course_phase_type_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY course_phase + ADD CONSTRAINT fk_phase_type FOREIGN KEY (course_phase_type_id) REFERENCES public.course_phase_type(id); +