From eed5888285dc6edc15e0392ff7e414046fc23724 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 16:14:26 +0100 Subject: [PATCH 01/10] adding go tesing workflow --- .github/workflows/go-tests.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/go-tests.yml diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml new file mode 100644 index 00000000..ccd1cb3c --- /dev/null +++ b/.github/workflows/go-tests.yml @@ -0,0 +1,27 @@ +name: Go Test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + directory: [ 'server' ] + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache-dependency-path: "**/*.sum" + - name: Install dependencies + run: cd ${{ matrix.directory }} && go mod download + - name: Test with Go + run: go test ./${{ matrix.directory }}/... -json > TestResults-${{ matrix.directory }}.json + - name: Upload Go test results + uses: actions/upload-artifact@v4 + with: + name: Go-results-${{ matrix.directory }} + path: TestResults-${{ matrix.directory }}.json \ No newline at end of file From f2ab32005923567dd848cd5eea6194662c16d4f8 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 16:27:04 +0100 Subject: [PATCH 02/10] fixing error --- server/application/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/application/main.go b/server/application/main.go index e69de29b..b584a8a4 100644 --- a/server/application/main.go +++ b/server/application/main.go @@ -0,0 +1 @@ +package application From 8a77da50dde516c2fa804d21cab408434dd7bdd9 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 16:34:24 +0100 Subject: [PATCH 03/10] adjusting testing script --- .github/workflows/go-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index ccd1cb3c..21754edc 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -19,9 +19,9 @@ jobs: - name: Install dependencies run: cd ${{ matrix.directory }} && go mod download - name: Test with Go - run: go test ./${{ matrix.directory }}/... -json > TestResults-${{ matrix.directory }}.json + run: cd ${{ matrix.directory }} && go test ./... -json > TestResults-${{ matrix.directory }}.json - name: Upload Go test results uses: actions/upload-artifact@v4 with: name: Go-results-${{ matrix.directory }} - path: TestResults-${{ matrix.directory }}.json \ No newline at end of file + path: ./${{ matrix.directory }}/TestResults-${{ matrix.directory }}.json \ No newline at end of file From 0365db95806ddf9a7f1fb9542cf6ecdf8e103da6 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 18:19:07 +0100 Subject: [PATCH 04/10] adding tests for course phase participation --- .../coursePhaseParticipation/router.go | 4 +- .../coursePhaseParticipation/router_test.go | 130 ++++++++++++++++++ .../coursePhaseParticipation/service_test.go | 99 +++++++++++++ .../validation_test.go | 58 ++++++++ .../course_phase_participation_test.sql | 53 +++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 server/coursePhase/coursePhaseParticipation/router_test.go create mode 100644 server/coursePhase/coursePhaseParticipation/service_test.go create mode 100644 server/coursePhase/coursePhaseParticipation/validation_test.go create mode 100644 server/database_dumps/course_phase_participation_test.sql diff --git a/server/coursePhase/coursePhaseParticipation/router.go b/server/coursePhase/coursePhaseParticipation/router.go index 36a9f5f9..d67abe28 100644 --- a/server/coursePhase/coursePhaseParticipation/router.go +++ b/server/coursePhase/coursePhaseParticipation/router.go @@ -11,8 +11,8 @@ import ( func setupCoursePhaseParticipationRouter(routerGroup *gin.RouterGroup) { courseParticipation := routerGroup.Group("/course_phases/:uuid/participations") - courseParticipation.GET("/", getParticipationsForCoursePhase) - courseParticipation.POST("/", createCoursePhaseParticipation) + courseParticipation.GET("", getParticipationsForCoursePhase) + courseParticipation.POST("", createCoursePhaseParticipation) courseParticipation.PUT("/:participation_uuid", updateCoursePhaseParticipation) } diff --git a/server/coursePhase/coursePhaseParticipation/router_test.go b/server/coursePhase/coursePhaseParticipation/router_test.go new file mode 100644 index 00000000..771a4f89 --- /dev/null +++ b/server/coursePhase/coursePhaseParticipation/router_test.go @@ -0,0 +1,130 @@ +package coursePhaseParticipation + +import ( + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseParticipation/coursePhaseParticipationDTO" + "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() + coursePhaseParticipationService CoursePhaseParticipationService +} + +func (suite *RouterTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../../database_dumps/course_phase_participation_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.coursePhaseParticipationService = CoursePhaseParticipationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CoursePhaseParticipationServiceSingleton = &suite.coursePhaseParticipationService + + suite.router = setupRouter() +} + +func (suite *RouterTestSuite) TearDownSuite() { + suite.cleanup() +} + +func setupRouter() *gin.Engine { + router := gin.Default() + api := router.Group("/api") + setupCoursePhaseParticipationRouter(api) + return router +} + +func (suite *RouterTestSuite) TestGetParticipationsForCoursePhase() { + req := httptest.NewRequest(http.MethodGet, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b/participations", nil) + w := httptest.NewRecorder() + + suite.router.ServeHTTP(w, req) + + assert.Equal(suite.T(), http.StatusOK, w.Code) + var participations []coursePhaseParticipationDTO.GetCoursePhaseParticipation + err := json.Unmarshal(w.Body.Bytes(), &participations) + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(participations), 0, "Expected participations to be returned") +} + +func (suite *RouterTestSuite) TestCreateCoursePhaseParticipation() { + jsonData := `{"a": "b"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + newParticipation := coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CourseParticipationID: uuid.MustParse("65dcc535-a9ab-4421-a2bc-0f09780ca59e"), + Passed: false, + MetaData: metaData, + } + body, _ := json.Marshal(newParticipation) + + req := httptest.NewRequest(http.MethodPost, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b/participations", 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 createdParticipation coursePhaseParticipationDTO.GetCoursePhaseParticipation + err = json.Unmarshal(w.Body.Bytes(), &createdParticipation) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), newParticipation.CourseParticipationID, createdParticipation.CourseParticipationID) +} + +func (suite *RouterTestSuite) TestUpdateCoursePhaseParticipation() { + jsonData := `{"other-value": "some skills"}` + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + updatedParticipation := coursePhaseParticipationDTO.UpdateCoursePhaseParticipation{ + ID: uuid.MustParse("7698f081-df55-4136-a58c-1a166bb1bbda"), + MetaData: metaData, + Passed: pgtype.Bool{Bool: true, Valid: true}, + } + body, _ := json.Marshal(updatedParticipation) + + req := httptest.NewRequest(http.MethodPut, "/api/course_phases/3d1f3b00-87f3-433b-a713-178c4050411b/participations/7698f081-df55-4136-a58c-1a166bb1bbda", 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) + var updated coursePhaseParticipationDTO.UpdateCoursePhaseParticipation + err = json.Unmarshal(w.Body.Bytes(), &updated) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), updatedParticipation.ID, updated.ID) + assert.Equal(suite.T(), updatedParticipation.Passed, updated.Passed) + assert.Equal(suite.T(), "some skills", updated.MetaData["other-value"]) + assert.Equal(suite.T(), "none", updated.MetaData["skills"]) +} + +func TestRouterTestSuite(t *testing.T) { + suite.Run(t, new(RouterTestSuite)) +} diff --git a/server/coursePhase/coursePhaseParticipation/service_test.go b/server/coursePhase/coursePhaseParticipation/service_test.go new file mode 100644 index 00000000..25e2430a --- /dev/null +++ b/server/coursePhase/coursePhaseParticipation/service_test.go @@ -0,0 +1,99 @@ +package coursePhaseParticipation + +import ( + "context" + "encoding/json" + "testing" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseParticipation/coursePhaseParticipationDTO" + "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 CoursePhaseParticipationTestSuite struct { + suite.Suite + ctx context.Context + cleanup func() + coursePhaseParticipationService CoursePhaseParticipationService +} + +func (suite *CoursePhaseParticipationTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../../database_dumps/course_phase_participation_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.coursePhaseParticipationService = CoursePhaseParticipationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CoursePhaseParticipationServiceSingleton = &suite.coursePhaseParticipationService +} + +func (suite *CoursePhaseParticipationTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CoursePhaseParticipationTestSuite) TestGetAllParticipationsForCoursePhase() { + coursePhaseID := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + + participations, err := GetAllParticipationsForCoursePhase(suite.ctx, coursePhaseID) + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(participations), 0, "Expected participations for the course phase") +} + +func (suite *CoursePhaseParticipationTestSuite) TestCreateCoursePhaseParticipation() { + jsonData := `{"a": "b"}` + // MetaData initialisieren + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + newParticipation := coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CoursePhaseID: uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b"), + CourseParticipationID: uuid.MustParse("65dcc535-a9ab-4421-a2bc-0f09780ca59e"), + Passed: false, + MetaData: metaData, + } + + createdParticipation, err := CreateCoursePhaseParticipation(suite.ctx, newParticipation) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), newParticipation.CoursePhaseID, createdParticipation.CoursePhaseID, "CoursePhaseID should match") + assert.Equal(suite.T(), newParticipation.MetaData, createdParticipation.MetaData, "Meta data should match") +} + +func (suite *CoursePhaseParticipationTestSuite) TestUpdateCoursePhaseParticipation() { + // Replace with a valid participation ID from your dump + participationID := uuid.MustParse("7698f081-df55-4136-a58c-1a166bb1bbda") + jsonData := `{"other-value": "some skills"}` + // MetaData initialisieren + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + updatedParticipation := coursePhaseParticipationDTO.UpdateCoursePhaseParticipation{ + ID: participationID, + MetaData: metaData, + Passed: pgtype.Bool{Bool: true, Valid: true}, + } + + result, err := UpdateCoursePhaseParticipation(suite.ctx, updatedParticipation) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), updatedParticipation.ID, result.ID, "Participation ID should match") + assert.Equal(suite.T(), updatedParticipation.Passed, result.Passed, "Passed data should match") + assert.Equal(suite.T(), updatedParticipation.MetaData["other-value"], result.MetaData["other-value"], "New Meta data should match") + assert.Equal(suite.T(), "none", result.MetaData["skills"], "Old Meta data should be unaffected - Meta data was not appended") +} + +func TestCoursePhaseParticipationTestSuite(t *testing.T) { + suite.Run(t, new(CoursePhaseParticipationTestSuite)) +} diff --git a/server/coursePhase/coursePhaseParticipation/validation_test.go b/server/coursePhase/coursePhaseParticipation/validation_test.go new file mode 100644 index 00000000..caff45f1 --- /dev/null +++ b/server/coursePhase/coursePhaseParticipation/validation_test.go @@ -0,0 +1,58 @@ +package coursePhaseParticipation + +import ( + "testing" + + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseParticipation/coursePhaseParticipationDTO" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + input coursePhaseParticipationDTO.CreateCoursePhaseParticipation + expectError bool + }{ + { + name: "Valid input", + input: coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CourseParticipationID: uuid.New(), + CoursePhaseID: uuid.New(), + }, + expectError: false, + }, + { + name: "Missing CourseParticipationID", + input: coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CourseParticipationID: uuid.Nil, + CoursePhaseID: uuid.New(), + }, + expectError: true, + }, + { + name: "Missing CoursePhaseID", + input: coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CourseParticipationID: uuid.New(), + CoursePhaseID: uuid.Nil, + }, + expectError: true, + }, + { + name: "Both IDs missing", + input: coursePhaseParticipationDTO.CreateCoursePhaseParticipation{ + CourseParticipationID: uuid.Nil, + CoursePhaseID: uuid.Nil, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(tt.input) + if (err != nil) != tt.expectError { + t.Errorf("Validate() error = %v, expectError = %v", err, tt.expectError) + } + }) + } +} diff --git a/server/database_dumps/course_phase_participation_test.sql b/server/database_dumps/course_phase_participation_test.sql new file mode 100644 index 00000000..8c107a36 --- /dev/null +++ b/server/database_dumps/course_phase_participation_test.sql @@ -0,0 +1,53 @@ +-- +-- 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_participation; Type: TABLE; Schema: public; Owner: prompt-postgres +-- + +CREATE TABLE course_phase_participation ( + id uuid NOT NULL, + course_participation_id uuid NOT NULL, + course_phase_id uuid NOT NULL, + passed boolean, + meta_data jsonb +); + + +-- +-- Data for Name: course_phase_participation; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course_phase_participation (id, course_participation_id, course_phase_id, passed, meta_data) +VALUES +('7cd22e70-34b6-4416-8c1a-54f899a35951', '6e19bab2-53d0-4b6a-ac02-33b23988401a', '3d1f3b00-87f3-433b-a713-178c4050411b', false, '{}'), +('71b5eff0-c3b7-4495-b37b-65fc211b4b69', '8713d7bc-1542-4366-88a9-1fa50945b052', '3d1f3b00-87f3-433b-a713-178c4050411b', false, '{}'), +('ba42a9bb-2130-45f2-9522-65d23501ef7c', '0e762fdd-c4fa-49f4-9c38-c90160cc6caa', '3d1f3b00-87f3-433b-a713-178c4050411b', false, '{}'), +('2c1d802c-f7c3-4ba0-b95f-f6a3edf91940', '0e762fdd-c4fa-49f4-9c38-c90160cc6caa', '500db7ed-2eb2-42d0-82b3-8750e12afa8a', false, '{}'), +('ed30f4b3-73e9-4867-a148-7d0c9cdef451', '6e19bab2-53d0-4b6a-ac02-33b23988401a', '500db7ed-2eb2-42d0-82b3-8750e12afa8a', false, '{}'), +('7698f081-df55-4136-a58c-1a166bb1bbda', '8713d7bc-1542-4366-88a9-1fa50945b052', '500db7ed-2eb2-42d0-82b3-8750e12afa8a', false, '{"skills": "none"}'); + + + +-- PostgreSQL database dump complete +-- + From f603c1e4090fa46c396fa0167b6a205786fcc7cb Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 19:30:04 +0100 Subject: [PATCH 05/10] 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 154b646a..8e319a90 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 00000000..df465053 --- /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 00000000..e04b2e4e --- /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 00000000..7e5a5eba --- /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 00000000..a4793949 --- /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); + From 256c9ac0d62d1c2711295295473fed8daebc28c3 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 19:39:11 +0100 Subject: [PATCH 06/10] improving meta data testing --- .../coursePhaseParticipation/service_test.go | 24 ++++++++++++++++- server/coursePhase/service_test.go | 27 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/server/coursePhase/coursePhaseParticipation/service_test.go b/server/coursePhase/coursePhaseParticipation/service_test.go index 25e2430a..b575cdb2 100644 --- a/server/coursePhase/coursePhaseParticipation/service_test.go +++ b/server/coursePhase/coursePhaseParticipation/service_test.go @@ -91,7 +91,29 @@ func (suite *CoursePhaseParticipationTestSuite) TestUpdateCoursePhaseParticipati assert.Equal(suite.T(), updatedParticipation.ID, result.ID, "Participation ID should match") assert.Equal(suite.T(), updatedParticipation.Passed, result.Passed, "Passed data should match") assert.Equal(suite.T(), updatedParticipation.MetaData["other-value"], result.MetaData["other-value"], "New Meta data should match") - assert.Equal(suite.T(), "none", result.MetaData["skills"], "Old Meta data should be unaffected - Meta data was not appended") + assert.Equal(suite.T(), meta.MetaData{"skills": "none", "other-value": "some skills"}, result.MetaData, "Old Meta data should be unaffected - Meta data was not appended") +} + +func (suite *CoursePhaseParticipationTestSuite) TestUpdateCoursePhaseParticipationWithMetaDataOverride() { + // Replace with a valid participation ID from your dump + participationID := uuid.MustParse("7698f081-df55-4136-a58c-1a166bb1bbda") + jsonData := `{"skills": "more than none", "other-value": "some skills"}` + // MetaData initialisieren + var metaData meta.MetaData + err := json.Unmarshal([]byte(jsonData), &metaData) + assert.NoError(suite.T(), err) + + updatedParticipation := coursePhaseParticipationDTO.UpdateCoursePhaseParticipation{ + ID: participationID, + MetaData: metaData, + Passed: pgtype.Bool{Bool: true, Valid: true}, + } + + result, err := UpdateCoursePhaseParticipation(suite.ctx, updatedParticipation) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), updatedParticipation.ID, result.ID, "Participation ID should match") + assert.Equal(suite.T(), updatedParticipation.Passed, result.Passed, "Passed data should match") + assert.Equal(suite.T(), updatedParticipation.MetaData, result.MetaData, "New Meta data should match") } func TestCoursePhaseParticipationTestSuite(t *testing.T) { diff --git a/server/coursePhase/service_test.go b/server/coursePhase/service_test.go index e04b2e4e..0f19c026 100644 --- a/server/coursePhase/service_test.go +++ b/server/coursePhase/service_test.go @@ -74,7 +74,32 @@ func (suite *CoursePhaseTestSuite) TestUpdateCoursePhase() { 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") + assert.Equal(suite.T(), meta.MetaData{"test-key": "test-value", "updated_key": "updated_value"}, updatedCoursePhase.MetaData, "Expected metadata to match updated data including the old data") +} + +func (suite *CoursePhaseTestSuite) TestUpdateCoursePhaseWithMetaDataOverride() { + id := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + jsonData := `{"test-key": "test-value-new", "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(), meta.MetaData{"test-key": "test-value-new", "updated_key": "updated_value"}, updatedCoursePhase.MetaData, "Expected metadata to match updated data including the old data") } func (suite *CoursePhaseTestSuite) TestCreateCoursePhase() { From e0fb309dca57814f7489a280fa215d3c556010e9 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Wed, 4 Dec 2024 20:54:41 +0100 Subject: [PATCH 07/10] adding tests to course participation --- server/course/courseParticipation/router.go | 3 +- .../course/courseParticipation/router_test.go | 112 ++++++++++++++++++ .../courseParticipation/service_test.go | 97 +++++++++++++++ .../courseParticipation/validation_test.go | 62 ++++++++++ .../course_participation_test.sql | 61 ++++++++++ 5 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 server/course/courseParticipation/router_test.go create mode 100644 server/course/courseParticipation/service_test.go create mode 100644 server/course/courseParticipation/validation_test.go create mode 100644 server/database_dumps/course_participation_test.sql diff --git a/server/course/courseParticipation/router.go b/server/course/courseParticipation/router.go index 98abfb9c..9bf0eb28 100644 --- a/server/course/courseParticipation/router.go +++ b/server/course/courseParticipation/router.go @@ -11,10 +11,11 @@ import ( func setupCourseParticipationRouter(router *gin.RouterGroup) { // incoming path should be /course/:uuid/ courseParticipation := router.Group("/courses/:uuid/participations") - courseParticipation.GET("/", getCourseParticipationsForCourse) + courseParticipation.GET("", getCourseParticipationsForCourse) courseParticipation.POST("/enroll", createCourseParticipation) } +// TODO: in future think about how to integrate / create "passed" students from previous phases func getCourseParticipationsForCourse(c *gin.Context) { id, err := uuid.Parse(c.Param("uuid")) if err != nil { diff --git a/server/course/courseParticipation/router_test.go b/server/course/courseParticipation/router_test.go new file mode 100644 index 00000000..020c130f --- /dev/null +++ b/server/course/courseParticipation/router_test.go @@ -0,0 +1,112 @@ +package courseParticipation + +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/course/courseParticipation/courseParticipationDTO" + "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() + courseParticipationService CourseParticipationService +} + +func (suite *RouterTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../../database_dumps/course_participation_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.courseParticipationService = CourseParticipationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CourseParticipationServiceSingleton = &suite.courseParticipationService + + suite.router = setupRouter() +} + +func (suite *RouterTestSuite) TearDownSuite() { + suite.cleanup() +} + +func setupRouter() *gin.Engine { + router := gin.Default() + api := router.Group("/api") + setupCourseParticipationRouter(api) + return router +} + +func (suite *RouterTestSuite) TestGetCourseParticipationsForCourse() { + courseID := "3f42d322-e5bf-4faa-b576-51f2cab14c2e" + + req := httptest.NewRequest(http.MethodGet, "/api/courses/"+courseID+"/participations", nil) + w := httptest.NewRecorder() + + suite.router.ServeHTTP(w, req) + + assert.Equal(suite.T(), http.StatusOK, w.Code) + + var participations []courseParticipationDTO.GetCourseParticipation + err := json.Unmarshal(w.Body.Bytes(), &participations) + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(participations), 0, "Expected participations for the course") + + expectedStudentIDs := []uuid.UUID{ + uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411a"), + uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744b"), + uuid.MustParse("500db7ed-2eb2-42d0-82b3-8750e12afa8b"), + } + + for i, participation := range participations { + assert.Equal(suite.T(), uuid.MustParse(courseID), participation.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), expectedStudentIDs[i], participation.StudentID, "Expected StudentID to match") + } +} + +func (suite *RouterTestSuite) TestCreateCourseParticipation() { + courseID := "918977e1-2d27-4b55-9064-8504ff027a1a" + newParticipation := courseParticipationDTO.CreateCourseParticipation{ + StudentID: uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411a"), + } + + body, _ := json.Marshal(newParticipation) + req := httptest.NewRequest(http.MethodPost, "/api/courses/"+courseID+"/participations/enroll", 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) + + var createdParticipation courseParticipationDTO.GetCourseParticipation + err := json.Unmarshal(w.Body.Bytes(), &createdParticipation) + assert.NoError(suite.T(), err) + + // Validate the created participation + assert.Equal(suite.T(), uuid.MustParse(courseID), createdParticipation.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), newParticipation.StudentID, createdParticipation.StudentID, "Expected StudentID to match") + assert.NotEqual(suite.T(), uuid.Nil, createdParticipation.ID, "Expected a valid UUID for the new participation") +} + +func TestRouterTestSuite(t *testing.T) { + suite.Run(t, new(RouterTestSuite)) +} diff --git a/server/course/courseParticipation/service_test.go b/server/course/courseParticipation/service_test.go new file mode 100644 index 00000000..8e52287d --- /dev/null +++ b/server/course/courseParticipation/service_test.go @@ -0,0 +1,97 @@ +package courseParticipation + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/course/courseParticipation/courseParticipationDTO" + "github.com/niclasheun/prompt2.0/testutils" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CourseParticipationTestSuite struct { + suite.Suite + ctx context.Context + cleanup func() + courseParticipationService CourseParticipationService +} + +func (suite *CourseParticipationTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../../database_dumps/course_participation_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.courseParticipationService = CourseParticipationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + CourseParticipationServiceSingleton = &suite.courseParticipationService +} + +func (suite *CourseParticipationTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CourseParticipationTestSuite) TestGetAllCourseParticipationsForCourse() { + courseID := uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e") + participations, err := GetAllCourseParticipationsForCourse(suite.ctx, courseID) + + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(participations), 0, "Expected participations for the course") + + expectedStudentIDs := []uuid.UUID{ + uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411a"), + uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744b"), + uuid.MustParse("500db7ed-2eb2-42d0-82b3-8750e12afa8b"), + } + + for i, participation := range participations { + assert.Equal(suite.T(), courseID, participation.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), expectedStudentIDs[i], participation.StudentID, "Expected StudentID to match") + } +} + +func (suite *CourseParticipationTestSuite) TestGetAllCourseParticipationsForStudent() { + studentID := uuid.MustParse("7dc1c4e8-4255-4874-80a0-0c12b958744b") + participations, err := GetAllCourseParticipationsForStudent(suite.ctx, studentID) + + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(participations), 0, "Expected participations for the student") + + expectedCourseIDs := []uuid.UUID{ + uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), + uuid.MustParse("918977e1-2d27-4b55-9064-8504ff027a1a"), + } + + for i, participation := range participations { + assert.Equal(suite.T(), studentID, participation.StudentID, "Expected StudentID to match") + assert.Equal(suite.T(), expectedCourseIDs[i], participation.CourseID, "Expected CourseID to match") + } +} + +func (suite *CourseParticipationTestSuite) TestCreateCourseParticipation() { + newParticipation := courseParticipationDTO.CreateCourseParticipation{ + CourseID: uuid.MustParse("918977e1-2d27-4b55-9064-8504ff027a1a"), + StudentID: uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411a"), + } + + createdParticipation, err := CreateCourseParticipation(suite.ctx, newParticipation) + assert.NoError(suite.T(), err) + + // Verify the created participation + assert.Equal(suite.T(), newParticipation.CourseID, createdParticipation.CourseID, "Expected CourseID to match") + assert.Equal(suite.T(), newParticipation.StudentID, createdParticipation.StudentID, "Expected StudentID to match") + assert.NotEqual(suite.T(), uuid.Nil, createdParticipation.ID, "Expected a valid UUID for the new participation") +} + +func TestCourseParticipationTestSuite(t *testing.T) { + suite.Run(t, new(CourseParticipationTestSuite)) +} diff --git a/server/course/courseParticipation/validation_test.go b/server/course/courseParticipation/validation_test.go new file mode 100644 index 00000000..972917d1 --- /dev/null +++ b/server/course/courseParticipation/validation_test.go @@ -0,0 +1,62 @@ +package courseParticipation + +import ( + "testing" + + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/course/courseParticipation/courseParticipationDTO" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + input courseParticipationDTO.CreateCourseParticipation + expectedError string + }{ + { + name: "valid course participation", + input: courseParticipationDTO.CreateCourseParticipation{ + CourseID: uuid.New(), + StudentID: uuid.New(), + }, + expectedError: "", + }, + { + name: "missing course ID", + input: courseParticipationDTO.CreateCourseParticipation{ + CourseID: uuid.Nil, + StudentID: uuid.New(), + }, + expectedError: "validation error: course id is required", + }, + { + name: "missing student ID", + input: courseParticipationDTO.CreateCourseParticipation{ + CourseID: uuid.New(), + StudentID: uuid.Nil, + }, + expectedError: "validation error: student id is required", + }, + { + name: "missing both course ID and student ID", + input: courseParticipationDTO.CreateCourseParticipation{ + CourseID: uuid.Nil, + StudentID: uuid.Nil, + }, + expectedError: "validation error: course id is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(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_participation_test.sql b/server/database_dumps/course_participation_test.sql new file mode 100644 index 00000000..7f6bf39b --- /dev/null +++ b/server/database_dumps/course_participation_test.sql @@ -0,0 +1,61 @@ +-- +-- 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_participation; Type: TABLE; Schema: public; Owner: prompt-postgres +-- + +CREATE TABLE course_participation ( + id uuid NOT NULL, + course_id uuid NOT NULL, + student_id uuid NOT NULL +); + + +-- +-- Data for Name: course_participation; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course_participation (id, course_id, student_id) VALUES ('6e19bab2-53d0-4b6a-ac02-33b23988401a', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', '3d1f3b00-87f3-433b-a713-178c4050411a'); +INSERT INTO course_participation (id, course_id, student_id) VALUES ('8713d7bc-1542-4366-88a9-1fa50945b052', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', '7dc1c4e8-4255-4874-80a0-0c12b958744b'); +INSERT INTO course_participation (id, course_id, student_id) VALUES ('0e762fdd-c4fa-49f4-9c38-c90160cc6caa', '3f42d322-e5bf-4faa-b576-51f2cab14c2e', '500db7ed-2eb2-42d0-82b3-8750e12afa8b'); +INSERT INTO course_participation (id, course_id, student_id) VALUES ('65dcc535-a9ab-4421-a2bc-0f09780ca59e', '918977e1-2d27-4b55-9064-8504ff027a1a', '500db7ed-2eb2-42d0-82b3-8750e12afa8b'); +INSERT INTO course_participation (id, course_id, student_id) VALUES ('ec679792-f9e1-423c-80fa-a9f3324fefa8', '918977e1-2d27-4b55-9064-8504ff027a1a', '7dc1c4e8-4255-4874-80a0-0c12b958744b'); +INSERT INTO course_participation (id, course_id, student_id) VALUES ('9f061396-e208-4b00-bc8c-f3a04bc0a212', '918977e1-2d27-4b55-9064-8504ff027a1a', '7dc1c4e8-4255-4874-80a0-0c12b958744a'); + + +-- +-- Name: course_participation course_participation_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_participation + ADD CONSTRAINT course_participation_pkey PRIMARY KEY (id); + + +-- +-- Name: course_participation unique_course_participation; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_participation + ADD CONSTRAINT unique_course_participation UNIQUE (course_id, student_id); + + From 01dddea28b82b863f8d4d9990a25259842265183 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Thu, 5 Dec 2024 09:52:32 +0100 Subject: [PATCH 08/10] removing phase meta data from course incl course phase graph --- .../shared_library/interfaces/course_phase.ts | 3 ++- server/course/service.go | 2 +- .../get_course_phase_sequence.go | 23 ++++++------------- server/db/query/course_graph.sql | 4 ++-- server/db/sqlc/course_graph.sql.go | 8 +++---- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/clients/shared_library/interfaces/course_phase.ts b/clients/shared_library/interfaces/course_phase.ts index 9e298fb8..67c39c31 100644 --- a/clients/shared_library/interfaces/course_phase.ts +++ b/clients/shared_library/interfaces/course_phase.ts @@ -2,7 +2,8 @@ export interface CoursePhase { id: string course_id: string name: string - meta_data: Array + //meta_data: Array This DTO is used for getting all courses + // in all courses we do not send the phase meta data is_initial_phase: boolean sequence_order: number course_phase_type_id: string diff --git a/server/course/service.go b/server/course/service.go index 0d0e5507..892b8ed8 100644 --- a/server/course/service.go +++ b/server/course/service.go @@ -100,7 +100,7 @@ func UpdateCoursePhaseOrder(ctx context.Context, courseID uuid.UUID, updatedPhas } // create new connections - for i, _ := range updatedPhaseOrder.OrderedPhases { + for i := range updatedPhaseOrder.OrderedPhases { if i < len(updatedPhaseOrder.OrderedPhases)-1 { err = CourseServiceSingleton.queries.CreateCourseGraphConnection(ctx, db.CreateCourseGraphConnectionParams{ FromCoursePhaseID: updatedPhaseOrder.OrderedPhases[i], diff --git a/server/coursePhase/coursePhaseDTO/get_course_phase_sequence.go b/server/coursePhase/coursePhaseDTO/get_course_phase_sequence.go index 9e2dc99b..22b48339 100644 --- a/server/coursePhase/coursePhaseDTO/get_course_phase_sequence.go +++ b/server/coursePhase/coursePhaseDTO/get_course_phase_sequence.go @@ -3,31 +3,23 @@ package coursePhaseDTO import ( "github.com/google/uuid" db "github.com/niclasheun/prompt2.0/db/sqlc" - "github.com/niclasheun/prompt2.0/meta" ) type CoursePhaseSequence struct { - ID uuid.UUID `json:"id"` - CourseID uuid.UUID `json:"course_id"` - Name string `json:"name"` - MetaData meta.MetaData `json:"meta_data"` - IsInitialPhase bool `json:"is_initial_phase"` - SequenceOrder int `json:"sequence_order"` - CoursePhaseTypeID uuid.UUID `json:"course_phase_type_id"` - CoursePhaseType string `json:"course_phase_type"` + ID uuid.UUID `json:"id"` + CourseID uuid.UUID `json:"course_id"` + Name string `json:"name"` + IsInitialPhase bool `json:"is_initial_phase"` + SequenceOrder int `json:"sequence_order"` + CoursePhaseTypeID uuid.UUID `json:"course_phase_type_id"` + CoursePhaseType string `json:"course_phase_type"` } func GetCoursePhaseSequenceDTOFromDBModel(model db.GetCoursePhaseSequenceRow) (CoursePhaseSequence, error) { - metaData, err := meta.GetMetaDataDTOFromDBModel(model.MetaData) - if err != nil { - return CoursePhaseSequence{}, err - } - return CoursePhaseSequence{ ID: model.ID, CourseID: model.CourseID, Name: model.Name.String, - MetaData: metaData, IsInitialPhase: model.IsInitialPhase, SequenceOrder: int(model.SequenceOrder), CoursePhaseTypeID: model.CoursePhaseTypeID, @@ -50,7 +42,6 @@ func GetCoursePhaseSequenceDTO(orderedPhases []db.GetCoursePhaseSequenceRow, not ID: phase.ID, CourseID: phase.CourseID, Name: phase.Name, - MetaData: phase.MetaData, IsInitialPhase: phase.IsInitialPhase, SequenceOrder: -1, CoursePhaseTypeID: phase.CoursePhaseTypeID, diff --git a/server/db/query/course_graph.sql b/server/db/query/course_graph.sql index 7760fe8b..b50b1650 100644 --- a/server/db/query/course_graph.sql +++ b/server/db/query/course_graph.sql @@ -1,12 +1,12 @@ -- name: GetCoursePhaseSequence :many WITH RECURSIVE phase_sequence AS ( - SELECT cp.*, 1 AS sequence_order + SELECT cp.id, cp.course_id, cp.name, cp.is_initial_phase, cp.course_phase_type_id, 1 AS sequence_order FROM course_phase cp WHERE cp.course_id = $1 AND cp.is_initial_phase = true UNION ALL - SELECT cp.*, ps.sequence_order + 1 AS sequence_order + SELECT cp.id, cp.course_id, cp.name, cp.is_initial_phase, cp.course_phase_type_id, ps.sequence_order + 1 AS sequence_order FROM course_phase cp INNER JOIN course_phase_graph g ON g.to_course_phase_id = cp.id INNER JOIN phase_sequence ps ON g.from_course_phase_id = ps.id diff --git a/server/db/sqlc/course_graph.sql.go b/server/db/sqlc/course_graph.sql.go index d94c5787..03d0156e 100644 --- a/server/db/sqlc/course_graph.sql.go +++ b/server/db/sqlc/course_graph.sql.go @@ -40,18 +40,18 @@ func (q *Queries) DeleteCourseGraph(ctx context.Context, courseID uuid.UUID) err const getCoursePhaseSequence = `-- name: GetCoursePhaseSequence :many WITH RECURSIVE phase_sequence AS ( - SELECT cp.id, cp.course_id, cp.name, cp.meta_data, cp.is_initial_phase, cp.course_phase_type_id, 1 AS sequence_order + SELECT cp.id, cp.course_id, cp.name, cp.is_initial_phase, cp.course_phase_type_id, 1 AS sequence_order FROM course_phase cp WHERE cp.course_id = $1 AND cp.is_initial_phase = true UNION ALL - SELECT cp.id, cp.course_id, cp.name, cp.meta_data, cp.is_initial_phase, cp.course_phase_type_id, ps.sequence_order + 1 AS sequence_order + SELECT cp.id, cp.course_id, cp.name, cp.is_initial_phase, cp.course_phase_type_id, ps.sequence_order + 1 AS sequence_order FROM course_phase cp INNER JOIN course_phase_graph g ON g.to_course_phase_id = cp.id INNER JOIN phase_sequence ps ON g.from_course_phase_id = ps.id ) -SELECT ps.id, ps.course_id, ps.name, ps.meta_data, ps.is_initial_phase, ps.course_phase_type_id, ps.sequence_order, cpt.name AS course_phase_type_name +SELECT ps.id, ps.course_id, ps.name, ps.is_initial_phase, ps.course_phase_type_id, ps.sequence_order, cpt.name AS course_phase_type_name FROM phase_sequence ps INNER JOIN course_phase_type cpt ON ps.course_phase_type_id = cpt.id ORDER BY ps.sequence_order @@ -61,7 +61,6 @@ type GetCoursePhaseSequenceRow struct { ID uuid.UUID `json:"id"` CourseID uuid.UUID `json:"course_id"` Name pgtype.Text `json:"name"` - MetaData []byte `json:"meta_data"` IsInitialPhase bool `json:"is_initial_phase"` CoursePhaseTypeID uuid.UUID `json:"course_phase_type_id"` SequenceOrder int32 `json:"sequence_order"` @@ -81,7 +80,6 @@ func (q *Queries) GetCoursePhaseSequence(ctx context.Context, courseID uuid.UUID &i.ID, &i.CourseID, &i.Name, - &i.MetaData, &i.IsInitialPhase, &i.CoursePhaseTypeID, &i.SequenceOrder, From 349d1641f63c47dffef93f5c9303e80eed5f0365 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Thu, 5 Dec 2024 11:20:08 +0100 Subject: [PATCH 09/10] adding most tests for course module --- server/course/service_test.go | 136 +++++++++++++++++++ server/course/validation.go | 10 ++ server/course/validation_test.go | 183 ++++++++++++++++++++++++++ server/database_dumps/course_test.sql | 154 ++++++++++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 server/course/service_test.go create mode 100644 server/course/validation_test.go create mode 100644 server/database_dumps/course_test.sql diff --git a/server/course/service_test.go b/server/course/service_test.go new file mode 100644 index 00000000..be03e353 --- /dev/null +++ b/server/course/service_test.go @@ -0,0 +1,136 @@ +package course + +import ( + "context" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/niclasheun/prompt2.0/course/courseDTO" + "github.com/niclasheun/prompt2.0/coursePhase" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseDTO" + db "github.com/niclasheun/prompt2.0/db/sqlc" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CourseServiceTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + courseService CourseService +} + +func (suite *CourseServiceTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/course_test.sql") + if err != nil { + suite.T().Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.courseService = CourseService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + CourseServiceSingleton = &suite.courseService + + // Initialize CoursePhase module + suite.router = gin.Default() + coursePhase.InitCoursePhaseModule(suite.router.Group("/api"), *testDB.Queries, testDB.Conn) +} + +func (suite *CourseServiceTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CourseServiceTestSuite) TestGetAllCourses() { + courses, err := GetAllCourses(suite.ctx) + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(courses), 0, "Expected at least one course") + + for _, course := range courses { + assert.NotEmpty(suite.T(), course.ID, "Course ID should not be empty") + assert.NotEmpty(suite.T(), course.Name, "Course Name should not be empty") + } +} + +func (suite *CourseServiceTestSuite) TestGetCourseByID() { + courseID := uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e") + + course, err := GetCourseByID(suite.ctx, courseID) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), courseID, course.ID, "Course ID should match") + assert.NotEmpty(suite.T(), course.CoursePhases, "Course should have phases") +} + +func (suite *CourseServiceTestSuite) TestCreateCourse() { + newCourse := courseDTO.CreateCourse{ + Name: "New Course", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "WS2024", Valid: true}, + MetaData: map[string]interface{}{"test_key": "test_value"}, + CourseType: db.CourseType("practical course"), + Ects: pgtype.Int4{Int32: 10, Valid: true}, + } + + createdCourse, err := CreateCourse(suite.ctx, newCourse) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), newCourse.Name, createdCourse.Name, "Course name should match") + assert.Equal(suite.T(), "practical course", createdCourse.CourseType, "Course type should match") +} + +func (suite *CourseServiceTestSuite) TestUpdateCoursePhaseOrder() { + courseID := uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e") + firstUUID := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + secondUUID := uuid.MustParse("500db7ed-2eb2-42d0-82b3-8750e12afa8a") + thirdUUID := uuid.MustParse("92bb0532-39e5-453d-bc50-fa61ea0128b2") + newPhaseOrder := courseDTO.CoursePhaseOrderRequest{ + OrderedPhases: []uuid.UUID{ + firstUUID, + secondUUID, + thirdUUID, + }, + } + + err := UpdateCoursePhaseOrder(suite.ctx, courseID, newPhaseOrder) + assert.NoError(suite.T(), err) + + // Verify phase order has been updated + course, err := GetCourseByID(suite.ctx, courseID) + assert.NoError(suite.T(), err) + var firstCoursePhase *coursePhaseDTO.CoursePhaseSequence + var secondCoursePhase *coursePhaseDTO.CoursePhaseSequence + var thirdCoursePhase *coursePhaseDTO.CoursePhaseSequence + + for _, phase := range course.CoursePhases { + if phase.SequenceOrder == 1 { + firstCoursePhase = &phase + } + if phase.SequenceOrder == 2 { + secondCoursePhase = &phase + } + if phase.SequenceOrder == 3 { + thirdCoursePhase = &phase + } + } + assert.Equal(suite.T(), firstUUID, firstCoursePhase.ID, "Phase order should match") + assert.Equal(suite.T(), secondUUID, secondCoursePhase.ID, "Phase order should match") + assert.Equal(suite.T(), thirdUUID, thirdCoursePhase.ID, "Phase order should match") + + assert.True(suite.T(), firstCoursePhase.IsInitialPhase, "First phase should be initial phase") + assert.False(suite.T(), secondCoursePhase.IsInitialPhase, "Second phase should not be initial phase") + assert.False(suite.T(), thirdCoursePhase.IsInitialPhase, "Third phase should not be initial phase") +} + +func TestCourseServiceTestSuite(t *testing.T) { + suite.Run(t, new(CourseServiceTestSuite)) +} diff --git a/server/course/validation.go b/server/course/validation.go index b025adff..80bfec7a 100644 --- a/server/course/validation.go +++ b/server/course/validation.go @@ -31,6 +31,16 @@ func validateCreateCourse(c courseDTO.CreateCourse) error { log.Error(errorMessage) return errors.New(errorMessage) } + if !c.SemesterTag.Valid || c.SemesterTag.String == "" { + errorMessage := "semester tag is required" + log.Error(errorMessage) + return errors.New(errorMessage) + } + if c.CourseType == "" { + errorMessage := "course type is required" + log.Error(errorMessage) + return errors.New(errorMessage) + } return nil } diff --git a/server/course/validation_test.go b/server/course/validation_test.go new file mode 100644 index 00000000..8960f483 --- /dev/null +++ b/server/course/validation_test.go @@ -0,0 +1,183 @@ +package course + +import ( + "context" + "log" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/niclasheun/prompt2.0/course/courseDTO" + "github.com/niclasheun/prompt2.0/coursePhase" + "github.com/niclasheun/prompt2.0/meta" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CourseTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + courseService CourseService +} + +func (suite *CourseTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/course_test.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.courseService = CourseService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + CourseServiceSingleton = &suite.courseService + suite.router = gin.Default() +} + +func (suite *CourseTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CourseTestSuite) TestValidateCreateCourse() { + tests := []struct { + name string + input courseDTO.CreateCourse + expectedError string + }{ + { + name: "valid course", + input: courseDTO.CreateCourse{ + Name: "Valid Course", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "WS2024", Valid: true}, + MetaData: meta.MetaData{"key": "value"}, + CourseType: "practical course", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + }, + expectedError: "", + }, + { + name: "missing course name", + input: courseDTO.CreateCourse{ + Name: "", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "WS2024", Valid: true}, + MetaData: meta.MetaData{"key": "value"}, + CourseType: "practical course", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + }, + expectedError: "course name is required", + }, + { + name: "start date after end date", + input: courseDTO.CreateCourse{ + Name: "Invalid Date Course", + StartDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + EndDate: pgtype.Date{Valid: true, Time: time.Now()}, + SemesterTag: pgtype.Text{String: "WS2024", Valid: true}, + MetaData: meta.MetaData{"key": "value"}, + CourseType: "practical course", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + }, + expectedError: "start date must be before end date", + }, + { + name: "missing semester tag", + input: courseDTO.CreateCourse{ + Name: "Invalid Semester Tag", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "", Valid: false}, + MetaData: meta.MetaData{"key": "value"}, + CourseType: "practical course", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + }, + expectedError: "semester tag is required", + }, + { + name: "missing course type", + input: courseDTO.CreateCourse{ + Name: "Invalid Course Type", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "ios2425", Valid: true}, + MetaData: meta.MetaData{"key": "value"}, + CourseType: "", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + }, + expectedError: "course type is required", + }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + err := validateCreateCourse(tt.input) + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, err, tt.expectedError) + } + }) + } +} + +func (suite *CourseTestSuite) TestValidateUpdateCourseOrder() { + // set up CoursePhaseService + coursePhase.InitCoursePhaseModule(suite.router.Group("/api"), suite.courseService.queries, suite.courseService.conn) + + tests := []struct { + name string + courseID uuid.UUID + orderedPhases []uuid.UUID + expectedError string + }{ + { + name: "valid course phase order", + courseID: uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), + orderedPhases: []uuid.UUID{ + uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b"), + uuid.MustParse("92bb0532-39e5-453d-bc50-fa61ea0128b2"), + }, + expectedError: "", + }, + // { + // name: "invalid course ID in phase", + // courseID: uuid.MustParse("3f42d322-e5bf-4faa-b576-51f2cab14c2e"), + // orderedPhases: []uuid.UUID{ + // uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411c"), + // }, + // expectedError: "course phase not found", + // }, + } + + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + err := validateUpdateCourseOrder(context.Background(), tt.courseID, courseDTO.CoursePhaseOrderRequest{ + OrderedPhases: tt.orderedPhases, + }) + if tt.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, err, tt.expectedError) + } + }) + } +} + +func TestCourseTestSuite(t *testing.T) { + suite.Run(t, new(CourseTestSuite)) +} diff --git a/server/database_dumps/course_test.sql b/server/database_dumps/course_test.sql new file mode 100644 index 00000000..e6deecd9 --- /dev/null +++ b/server/database_dumps/course_test.sql @@ -0,0 +1,154 @@ +-- +-- 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; Type: TABLE; Schema: public; Owner: prompt-postgres +-- + +create type course_type as enum ('lecture', 'seminar', 'practical course'); + + +CREATE TABLE course ( + id uuid NOT NULL, + name text NOT NULL, + start_date date, + end_date date, + semester_tag text, + course_type course_type NOT NULL, + ects integer, + meta_data jsonb +); + + +-- +-- Data for Name: course; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('3f42d322-e5bf-4faa-b576-51f2cab14c2e', 'iPraktikum', '2024-10-01', '2025-01-01', 'ios24245', 'practical course', 10, '{"icon": "apple", "bg-color": "bg-orange-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('918977e1-2d27-4b55-9064-8504ff027a1a', 'New fancy course', '2024-10-01', '2025-01-01', 'ios24245', 'practical course', 10, '{}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('fe672868-3d07-4bdd-af41-121fd05e2d0d', 'iPraktikum', '2024-10-01', '2025-01-01', 'ios24245', 'lecture', 5, '{"icon": "home", "color": "green"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('0bb5c8dc-a4df-4d64-a9fd-fe8840760d6b', 'Test5', '2025-01-13', '2025-01-18', 'ios2425', 'seminar', 5, '{"icon": "smartphone", "bg-color": "bg-blue-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('55856fdc-fc2f-456a-a5a5-726d60aaae7c', 'iPraktikum3', '2025-01-07', '2025-01-24', 'ios2425', 'practical course', 10, '{"icon": "smartphone", "bg-color": "bg-green-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('64a12e61-a238-4cea-a36a-5eaf89d7a940', 'Another TEst', '2024-12-15', '2025-01-17', 'ios2425', 'seminar', 5, '{"icon": "folder", "bg-color": "bg-red-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('07d0664c-6116-4897-97c9-521c8d73dd9f', 'Further Testing', '2024-12-17', '2025-01-15', 'ios24', 'practical course', 10, '{"icon": "monitor", "bg-color": "bg-cyan-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('00f6d242-9716-487c-a8de-5e02112ea131', 'Test150', '2024-12-17', '2025-01-17', 'test', 'practical course', 10, '{"icon": "book-open-text", "bg-color": "bg-orange-100"}'); +INSERT INTO course (id, name, start_date, end_date, semester_tag, course_type, ects, meta_data) VALUES ('894cb6fc-9407-4642-b4de-2e0b4e893126', 'iPraktikum-Test', '2025-03-10', '2025-08-01', 'ios2425', 'practical course', 10, '{"icon": "gamepad-2", "bg-color": "bg-green-100"}'); + + +-- +-- Name: course course_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course + ADD CONSTRAINT course_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + + +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 course_phase_type(id); + + +CREATE TABLE course_phase_graph ( + from_course_phase_id uuid NOT NULL, + to_course_phase_id uuid NOT NULL +); + +-- +-- Data for Name: course_phase_graph; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course_phase_graph (from_course_phase_id, to_course_phase_id) VALUES ('500db7ed-2eb2-42d0-82b3-8750e12afa8a', '92bb0532-39e5-453d-bc50-fa61ea0128b2'); + + +-- +-- Name: course_phase_graph unique_from_course_phase; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_graph + ADD CONSTRAINT unique_from_course_phase UNIQUE (from_course_phase_id); + + +-- +-- Name: course_phase_graph unique_to_course_phase; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_graph + ADD CONSTRAINT unique_to_course_phase UNIQUE (to_course_phase_id); + + +-- +-- Name: course_phase_graph fk_from_course_phase; Type: FK CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_graph + ADD CONSTRAINT fk_from_course_phase FOREIGN KEY (from_course_phase_id) REFERENCES course_phase(id) ON DELETE CASCADE; + + +-- +-- Name: course_phase_graph fk_to_course_phase; Type: FK CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_graph + ADD CONSTRAINT fk_to_course_phase FOREIGN KEY (to_course_phase_id) REFERENCES course_phase(id) ON DELETE CASCADE; + From 14f956d01b8266acf4c07944e74e3d954de26fde Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Thu, 5 Dec 2024 11:29:31 +0100 Subject: [PATCH 10/10] adding router test and changing workflow to run on pull request --- .github/workflows/go-tests.yml | 4 +- server/course/router_test.go | 180 +++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 server/course/router_test.go diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index 21754edc..66e12e7b 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -1,6 +1,8 @@ name: Go Test -on: [push] +on: + pull_request: + branches: [main] jobs: test: diff --git a/server/course/router_test.go b/server/course/router_test.go new file mode 100644 index 00000000..655a1df4 --- /dev/null +++ b/server/course/router_test.go @@ -0,0 +1,180 @@ +package course + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/niclasheun/prompt2.0/course/courseDTO" + "github.com/niclasheun/prompt2.0/coursePhase" + "github.com/niclasheun/prompt2.0/coursePhase/coursePhaseDTO" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type CourseRouterTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + courseService CourseService +} + +func (suite *CourseRouterTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/course_test.sql") + if err != nil { + suite.T().Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.courseService = CourseService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + CourseServiceSingleton = &suite.courseService + + // Initialize router + suite.router = gin.Default() + api := suite.router.Group("/api") + setupCourseRouter(api) + + coursePhase.InitCoursePhaseModule(api, *testDB.Queries, testDB.Conn) +} + +func (suite *CourseRouterTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *CourseRouterTestSuite) TestGetAllCourses() { + req, _ := http.NewRequest("GET", "/api/courses/", nil) + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + + var courses []courseDTO.CourseWithPhases + err := json.Unmarshal(resp.Body.Bytes(), &courses) + assert.NoError(suite.T(), err) + assert.Greater(suite.T(), len(courses), 0, "Expected at least one course") +} + +func (suite *CourseRouterTestSuite) TestGetCourseByID() { + courseID := "3f42d322-e5bf-4faa-b576-51f2cab14c2e" + req, _ := http.NewRequest("GET", "/api/courses/"+courseID, nil) + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + + var course courseDTO.CourseWithPhases + err := json.Unmarshal(resp.Body.Bytes(), &course) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), uuid.MustParse(courseID), course.ID, "Course ID should match") + assert.NotEmpty(suite.T(), course.CoursePhases, "Course should have phases") +} + +func (suite *CourseRouterTestSuite) TestCreateCourse() { + newCourse := courseDTO.CreateCourse{ + Name: "Router Test Course", + StartDate: pgtype.Date{Valid: true, Time: time.Now()}, + EndDate: pgtype.Date{Valid: true, Time: time.Now().Add(24 * time.Hour)}, + SemesterTag: pgtype.Text{String: "WS2024", Valid: true}, + MetaData: map[string]interface{}{"icon": "test-icon"}, + CourseType: "practical course", + Ects: pgtype.Int4{Int32: 10, Valid: true}, + } + body, _ := json.Marshal(newCourse) + req, _ := http.NewRequest("POST", "/api/courses/", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusCreated, resp.Code) + + var createdCourse courseDTO.Course + err := json.Unmarshal(resp.Body.Bytes(), &createdCourse) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), newCourse.Name, createdCourse.Name, "Course name should match") + assert.Equal(suite.T(), "practical course", createdCourse.CourseType, "Course type should match") +} + +func (suite *CourseRouterTestSuite) TestUpdateCoursePhaseOrder() { + courseID := "3f42d322-e5bf-4faa-b576-51f2cab14c2e" + firstUUID := uuid.MustParse("3d1f3b00-87f3-433b-a713-178c4050411b") + secondUUID := uuid.MustParse("500db7ed-2eb2-42d0-82b3-8750e12afa8a") + thirdUUID := uuid.MustParse("92bb0532-39e5-453d-bc50-fa61ea0128b2") + newPhaseOrder := courseDTO.CoursePhaseOrderRequest{ + OrderedPhases: []uuid.UUID{ + firstUUID, + secondUUID, + thirdUUID, + }, + } + body, _ := json.Marshal(newPhaseOrder) + req, _ := http.NewRequest("PUT", "/api/courses/"+courseID+"/phase_graph", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + + // Verify the updated order + req, _ = http.NewRequest("GET", "/api/courses/"+courseID, nil) + resp = httptest.NewRecorder() + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + + var updatedCourse courseDTO.CourseWithPhases + err := json.Unmarshal(resp.Body.Bytes(), &updatedCourse) + assert.NoError(suite.T(), err) + + // Validate the phases in the updated course + var firstCoursePhase *coursePhaseDTO.CoursePhaseSequence + var secondCoursePhase *coursePhaseDTO.CoursePhaseSequence + var thirdCoursePhase *coursePhaseDTO.CoursePhaseSequence + + for _, phase := range updatedCourse.CoursePhases { + if phase.SequenceOrder == 1 { + firstCoursePhase = &phase + } + if phase.SequenceOrder == 2 { + secondCoursePhase = &phase + } + if phase.SequenceOrder == 3 { + thirdCoursePhase = &phase + } + } + + assert.NotNil(suite.T(), firstCoursePhase, "First phase should be present") + assert.NotNil(suite.T(), secondCoursePhase, "Second phase should be present") + assert.NotNil(suite.T(), thirdCoursePhase, "Third phase should be present") + + assert.Equal(suite.T(), firstUUID, firstCoursePhase.ID, "First phase ID should match") + assert.Equal(suite.T(), secondUUID, secondCoursePhase.ID, "Second phase ID should match") + assert.Equal(suite.T(), thirdUUID, thirdCoursePhase.ID, "Third phase ID should match") + + assert.True(suite.T(), firstCoursePhase.IsInitialPhase, "First phase should be initial phase") + assert.False(suite.T(), secondCoursePhase.IsInitialPhase, "Second phase should not be initial phase") + assert.False(suite.T(), thirdCoursePhase.IsInitialPhase, "Third phase should not be initial phase") +} + +func TestCourseRouterTestSuite(t *testing.T) { + suite.Run(t, new(CourseRouterTestSuite)) +}