From 9b3cd43ff482231ba5637e06fac4e6f572f3c552 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 09:01:04 +0100 Subject: [PATCH 1/7] adding db dump and validation tests --- .../applicationAdministration/validation.go | 24 ++- .../validation_test.go | 155 +++++++++++++++ .../application_administration.sql | 180 ++++++++++++++++++ 3 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 server/applicationAdministration/validation_test.go create mode 100644 server/database_dumps/application_administration.sql diff --git a/server/applicationAdministration/validation.go b/server/applicationAdministration/validation.go index c14cc82..b69eabb 100644 --- a/server/applicationAdministration/validation.go +++ b/server/applicationAdministration/validation.go @@ -9,15 +9,27 @@ import ( "github.com/google/uuid" "github.com/niclasheun/prompt2.0/applicationAdministration/applicationDTO" db "github.com/niclasheun/prompt2.0/db/sqlc" + "github.com/sirupsen/logrus" ) func validateUpdateForm(ctx context.Context, coursePhaseID uuid.UUID, updateForm applicationDTO.UpdateForm) error { ctxWithTimeout, cancel := db.GetTimeoutContext(ctx) defer cancel() + // Check if course phase is application phase + isApplicationPhase, err := ApplicationServiceSingleton.queries.CheckIfCoursePhaseIsApplicationPhase(ctxWithTimeout, coursePhaseID) + if err != nil { + logrus.Error("could not validate application form: ", err) + return errors.New("could not validate the application form") + } + if !isApplicationPhase { + return errors.New("course phase is not an application phase") + } + // Get all questions for the course phase applicationQuestionsText, err := ApplicationServiceSingleton.queries.GetApplicationQuestionsTextForCoursePhase(ctxWithTimeout, coursePhaseID) if err != nil { + logrus.Error("could not validate application form: ", err) return errors.New("could not validate the application form") } @@ -38,14 +50,14 @@ func validateUpdateForm(ctx context.Context, coursePhaseID uuid.UUID, updateForm } // 1. DELETE: Check that all deleted questions are from this course - for _, question := range updateForm.DeleteQuestionsText { - if !textQuestionsMap[question] { + for _, questionID := range updateForm.DeleteQuestionsText { + if !textQuestionsMap[questionID] { return errors.New("question does not belong to this course") } } - for _, question := range updateForm.DeleteQuestionsMultiSelect { - if !multiSelectQuestionsMap[question] { + for _, questionID := range updateForm.DeleteQuestionsMultiSelect { + if !multiSelectQuestionsMap[questionID] { return errors.New("question does not belong to this course") } } @@ -135,6 +147,10 @@ func validateQuestionMultiSelect(title string, minSelect, maxSelect int, options return errors.New("maximum selection must be at least 1") } + if maxSelect < minSelect { + return errors.New("maximum selection must be greater than or equal to minimum selection") + } + // Ensure options are not empty if len(options) == 0 { return errors.New("options cannot be empty") diff --git a/server/applicationAdministration/validation_test.go b/server/applicationAdministration/validation_test.go new file mode 100644 index 0000000..9ef3228 --- /dev/null +++ b/server/applicationAdministration/validation_test.go @@ -0,0 +1,155 @@ +package applicationAdministration + +import ( + "context" + "log" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/applicationAdministration/applicationDTO" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ApplicationAdministrationTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + applicationAdminService ApplicationService +} + +func (suite *ApplicationAdministrationTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/application_administration.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.applicationAdminService = ApplicationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + ApplicationServiceSingleton = &suite.applicationAdminService + suite.router = gin.Default() +} + +func (suite *ApplicationAdministrationTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_Success() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + updateForm := applicationDTO.UpdateForm{ + DeleteQuestionsText: []uuid.UUID{uuid.MustParse("a6a04042-95d1-4765-8592-caf9560c8c3c")}, + DeleteQuestionsMultiSelect: []uuid.UUID{uuid.MustParse("65e25b73-ce47-4536-b651-a1632347d733")}, + CreateQuestionsText: []applicationDTO.CreateQuestionText{ + { + CoursePhaseID: coursePhaseID, + Title: "Valid Title", + AllowedLength: 100, + }, + }, + CreateQuestionsMultiSelect: []applicationDTO.CreateQuestionMultiSelect{ + { + CoursePhaseID: coursePhaseID, + Title: "Valid MultiSelect", + MinSelect: 1, + MaxSelect: 3, + Options: []string{"Option1", "Option2"}, + }, + }, + } + err := validateUpdateForm(suite.ctx, coursePhaseID, updateForm) + assert.NoError(suite.T(), err) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidDeleteQuestion() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + updateForm := applicationDTO.UpdateForm{ + DeleteQuestionsText: []uuid.UUID{uuid.New()}, // Non-existent question + } + err := validateUpdateForm(suite.ctx, coursePhaseID, updateForm) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "question does not belong to this course", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidCreateTextQuestion() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + updateForm := applicationDTO.UpdateForm{ + CreateQuestionsText: []applicationDTO.CreateQuestionText{ + { + CoursePhaseID: uuid.New(), + Title: "", + AllowedLength: 0, + }, + }, + } + err := validateUpdateForm(suite.ctx, coursePhaseID, updateForm) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "course phase id is not correct", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidCreateMultiSelect() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + updateForm := applicationDTO.UpdateForm{ + CreateQuestionsMultiSelect: []applicationDTO.CreateQuestionMultiSelect{ + { + CoursePhaseID: coursePhaseID, + Title: "Invalid MultiSelect", + MinSelect: -1, + MaxSelect: 0, + Options: []string{}, + }, + }, + } + err := validateUpdateForm(suite.ctx, coursePhaseID, updateForm) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "minimum selection must be at least 0", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionText_EmptyTitle() { + err := validateQuestionText("", "", 100) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "title is required", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionText_InvalidRegex() { + err := validateQuestionText("Valid Title", "[", 100) + assert.Error(suite.T(), err) + assert.Contains(suite.T(), err.Error(), "invalid regex pattern") +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_EmptyTitle() { + err := validateQuestionMultiSelect("", 1, 3, []string{"Option1", "Option2"}) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "title is required", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_EmptyOptions() { + err := validateQuestionMultiSelect("Valid Title", 1, 3, []string{}) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "options cannot be empty", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_MinSelectNegative() { + err := validateQuestionMultiSelect("Valid Title", -1, 3, []string{"Option1", "Option2"}) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "minimum selection must be at least 0", err.Error()) +} + +func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_MaxSelectLessThanOne() { + err := validateQuestionMultiSelect("Valid Title", 0, 0, []string{"Option1", "Option2"}) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "maximum selection must be at least 1", err.Error()) +} + +func TestValidateUpdateFormSuite(t *testing.T) { + suite.Run(t, new(ApplicationAdministrationTestSuite)) +} diff --git a/server/database_dumps/application_administration.sql b/server/database_dumps/application_administration.sql new file mode 100644 index 0000000..daf2938 --- /dev/null +++ b/server/database_dumps/application_administration.sql @@ -0,0 +1,180 @@ +-- +-- 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; Type: TABLE; Schema: public; Owner: prompt-postgres +-- + +CREATE TABLE course_phase_type ( + id uuid NOT NULL, + name text NOT NULL, + required_input_meta_data jsonb DEFAULT '{}'::jsonb NOT NULL, + provided_output_meta_data jsonb DEFAULT '{}'::jsonb NOT NULL, + initial_phase boolean DEFAULT false NOT NULL +); + +-- +-- Data for Name: course_phase_type; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO course_phase_type (id, name, required_input_meta_data, provided_output_meta_data, initial_phase) VALUES ('48d22f19-6cc0-417b-ac25-415fb40f2030', 'Intro Course', '[{"name": "hasOwnMac", "type": "boolean"}]', '[{"name": "proficiency level", "type": "string"}]', false); +INSERT INTO course_phase_type (id, name, required_input_meta_data, provided_output_meta_data, initial_phase) VALUES ('96fb1001-b21c-4527-8b6f-2fd5f4ba3abc', 'Application', '[]', '[{"name": "hasOwnMac", "type": "boolean"}, {"name": "devices", "type": "array"}]', true); +INSERT INTO course_phase_type (id, name, required_input_meta_data, provided_output_meta_data, initial_phase) VALUES ('627b6fb9-2106-4fce-ba6d-b68eeb546382', 'Team Phase', '[{"name": "proficiency level", "type": "string"}, {"name": "devices", "type": "array"}]', '[]', false); + + +-- +-- Name: course_phase_type course_phase_type_name_key; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_type + ADD CONSTRAINT course_phase_type_name_key UNIQUE (name); + + +-- +-- Name: course_phase_type course_phase_type_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase_type + ADD CONSTRAINT course_phase_type_pkey PRIMARY KEY (id); + + +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 +); + +-- +-- 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 ('4179d58a-d00d-4fa7-94a5-397bc69fab02', 'be780b32-a678-4b79-ae1c-80071771d254', 'Dev Application', '{"applicationEndDate": "2025-01-18T00:00:00.000Z", "applicationStartDate": "2024-12-24T00:00:00.000Z", "externalStudentsAllowed": false}', true, '96fb1001-b21c-4527-8b6f-2fd5f4ba3abc'); +INSERT INTO course_phase (id, course_id, name, meta_data, is_initial_phase, course_phase_type_id) VALUES ('7062236a-e290-487c-be41-29b24e0afc64', 'e12ffe63-448d-4469-a840-1699e9b328d1', 'New Team Phase', '{}', false, '627b6fb9-2106-4fce-ba6d-b68eeb546382'); +INSERT INTO course_phase (id, course_id, name, meta_data, is_initial_phase, course_phase_type_id) VALUES ('e12ffe63-448d-4469-a840-1699e9b328d3', 'e12ffe63-448d-4469-a840-1699e9b328d1', 'Intro Course', '{}', false, '48d22f19-6cc0-417b-ac25-415fb40f2030'); + + +-- +-- Name: course_phase course_phase_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase + ADD CONSTRAINT course_phase_pkey PRIMARY KEY (id); + + +-- +-- Name: unique_initial_phase_per_course; Type: INDEX; Schema: public; Owner: prompt-postgres +-- + +CREATE UNIQUE INDEX unique_initial_phase_per_course ON course_phase USING btree (course_id) WHERE (is_initial_phase = true); + + +-- +-- Name: course_phase fk_phase_type; Type: FK CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY course_phase + ADD CONSTRAINT fk_phase_type FOREIGN KEY (course_phase_type_id) REFERENCES course_phase_type(id); + + +CREATE TABLE application_question_multi_select ( + id uuid NOT NULL, + course_phase_id uuid NOT NULL, + title text, + description text, + placeholder text, + error_message text, + is_required boolean, + min_select integer, + max_select integer, + options text[], + order_num integer +); + + +-- +-- Data for Name: application_question_multi_select; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO application_question_multi_select (id, course_phase_id, title, description, placeholder, error_message, is_required, min_select, max_select, options, order_num) VALUES ('65e25b73-ce47-4536-b651-a1632347d733', '4179d58a-d00d-4fa7-94a5-397bc69fab02', 'Taken Courses', 'Which courses have you already taken ad the chair', '', '', false, 0, 3, '{Ferienakademie,Patterns,"Interactive Learning"}', 4); +INSERT INTO application_question_multi_select (id, course_phase_id, title, description, placeholder, error_message, is_required, min_select, max_select, options, order_num) VALUES ('383a9590-fba2-4e6b-a32b-88895d55fb9b', '4179d58a-d00d-4fa7-94a5-397bc69fab02', 'Available Devices', '', '', '', false, 0, 4, '{iPhone,iPad,MacBook,Vision}', 2); + + +-- +-- Name: application_question_multi_select application_question_multi_select_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY application_question_multi_select + ADD CONSTRAINT application_question_multi_select_pkey PRIMARY KEY (id); + + +-- +-- Name: application_question_multi_select fk_course_phase; Type: FK CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY application_question_multi_select + ADD CONSTRAINT fk_course_phase FOREIGN KEY (course_phase_id) REFERENCES course_phase(id) ON DELETE CASCADE; + + +CREATE TABLE application_question_text ( + id uuid NOT NULL, + course_phase_id uuid NOT NULL, + title text, + description text, + placeholder text, + validation_regex text, + error_message text, + is_required boolean, + allowed_length integer, + order_num integer +); + +-- +-- Data for Name: application_question_text; Type: TABLE DATA; Schema: public; Owner: prompt-postgres +-- + +INSERT INTO application_question_text (id, course_phase_id, title, description, placeholder, validation_regex, error_message, is_required, allowed_length, order_num) VALUES ('a6a04042-95d1-4765-8592-caf9560c8c3c', '4179d58a-d00d-4fa7-94a5-397bc69fab02', 'Motivation', 'You should fill out the motivation why you want to take this absolutely amazing course.', 'Enter your motivation.', '', 'You are not allowed to enter more than 500 chars. ', true, 500, 3); +INSERT INTO application_question_text (id, course_phase_id, title, description, placeholder, validation_regex, error_message, is_required, allowed_length, order_num) VALUES ('fc8bda6d-280e-4a5e-9ebd-4bd8b68aab75', '4179d58a-d00d-4fa7-94a5-397bc69fab02', 'Expierence', '', '', '', '', false, 500, 1); + + +-- +-- Name: application_question_text application_question_text_pkey; Type: CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY application_question_text + ADD CONSTRAINT application_question_text_pkey PRIMARY KEY (id); + + +-- +-- Name: application_question_text fk_course_phase; Type: FK CONSTRAINT; Schema: public; Owner: prompt-postgres +-- + +ALTER TABLE ONLY application_question_text + ADD CONSTRAINT fk_course_phase FOREIGN KEY (course_phase_id) REFERENCES course_phase(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + From 7285979a4560c2f6ce152a9fd498518b4d72afaf Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 09:20:54 +0100 Subject: [PATCH 2/7] writing service tests --- server/applicationAdministration/service.go | 5 - .../applicationAdministration/service_test.go | 156 ++++++++++++++++++ .../validation_test.go | 28 ++-- 3 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 server/applicationAdministration/service_test.go diff --git a/server/applicationAdministration/service.go b/server/applicationAdministration/service.go index bade171..1b4e23e 100644 --- a/server/applicationAdministration/service.go +++ b/server/applicationAdministration/service.go @@ -127,8 +127,3 @@ func UpdateApplicationForm(ctx context.Context, coursePhaseId uuid.UUID, form ap return nil } - -// TODO -func GetApplication() { - -} diff --git a/server/applicationAdministration/service_test.go b/server/applicationAdministration/service_test.go new file mode 100644 index 0000000..489156e --- /dev/null +++ b/server/applicationAdministration/service_test.go @@ -0,0 +1,156 @@ +package applicationAdministration + +import ( + "context" + "log" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/niclasheun/prompt2.0/applicationAdministration/applicationDTO" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ApplicationAdminServiceTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + applicationAdminService ApplicationService +} + +func (suite *ApplicationAdminServiceTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/application_administration.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.applicationAdminService = ApplicationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + ApplicationServiceSingleton = &suite.applicationAdminService + suite.router = gin.Default() +} + +func (suite *ApplicationAdminServiceTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *ApplicationAdminServiceTestSuite) TestGetApplicationForm_Success() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + + form, err := GetApplicationForm(suite.ctx, coursePhaseID) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), form) + assert.NotEmpty(suite.T(), form.QuestionsText) + assert.NotEmpty(suite.T(), form.QuestionsMultiSelect) + + // Verify QuestionsText + assert.Equal(suite.T(), 2, len(form.QuestionsText)) + for _, question := range form.QuestionsText { + if question.Title == "Motivation" { + assert.Equal(suite.T(), 500, question.AllowedLength) + } else if question.Title == "Expierence" { + assert.Equal(suite.T(), 500, question.AllowedLength) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } + + // Verify QuestionsMultiSelect + assert.Equal(suite.T(), 2, len(form.QuestionsMultiSelect)) + for _, question := range form.QuestionsMultiSelect { + if question.Title == "Taken Courses" { + assert.ElementsMatch(suite.T(), []string{"Ferienakademie", "Patterns", "Interactive Learning"}, question.Options) + } else if question.Title == "Available Devices" { + assert.ElementsMatch(suite.T(), []string{"iPhone", "iPad", "MacBook", "Vision"}, question.Options) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } +} + +func (suite *ApplicationAdminServiceTestSuite) TestGetApplicationForm_NotApplicationPhase() { + nonApplicationPhaseID := uuid.MustParse("7062236a-e290-487c-be41-29b24e0afc64") + + _, err := GetApplicationForm(suite.ctx, nonApplicationPhaseID) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "course phase is not an application phase", err.Error()) +} + +func (suite *ApplicationAdminServiceTestSuite) TestUpdateApplicationForm_Success() { + coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") + updateForm := applicationDTO.UpdateForm{ + DeleteQuestionsText: []uuid.UUID{uuid.MustParse("a6a04042-95d1-4765-8592-caf9560c8c3c")}, + DeleteQuestionsMultiSelect: []uuid.UUID{uuid.MustParse("383a9590-fba2-4e6b-a32b-88895d55fb9b")}, + CreateQuestionsText: []applicationDTO.CreateQuestionText{ + { + CoursePhaseID: coursePhaseID, + Title: "New Motivation", + AllowedLength: 300, + }, + }, + CreateQuestionsMultiSelect: []applicationDTO.CreateQuestionMultiSelect{ + { + CoursePhaseID: coursePhaseID, + Title: "New Devices", + MinSelect: 1, + MaxSelect: 5, + Options: []string{"Option1", "Option2"}, + }, + }, + } + + err := UpdateApplicationForm(suite.ctx, coursePhaseID, updateForm) + assert.NoError(suite.T(), err) + + // Verify updates + form, err := GetApplicationForm(suite.ctx, coursePhaseID) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), form) + + // Verify QuestionsText + assert.Equal(suite.T(), 2, len(form.QuestionsText)) + for _, question := range form.QuestionsText { + if question.Title == "New Motivation" { + assert.Equal(suite.T(), 300, question.AllowedLength) + } else if question.Title == "Expierence" { + assert.Equal(suite.T(), 500, question.AllowedLength) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } + + // Verify QuestionsMultiSelect + assert.Equal(suite.T(), 2, len(form.QuestionsMultiSelect)) + for _, question := range form.QuestionsMultiSelect { + if question.Title == "New Devices" { + assert.ElementsMatch(suite.T(), []string{"Option1", "Option2"}, question.Options) + } else if question.Title == "Taken Courses" { + assert.ElementsMatch(suite.T(), []string{"Ferienakademie", "Patterns", "Interactive Learning"}, question.Options) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } +} + +func (suite *ApplicationAdminServiceTestSuite) TestUpdateApplicationForm_NotApplicationPhase() { + nonApplicationPhaseID := uuid.MustParse("7062236a-e290-487c-be41-29b24e0afc64") + updateForm := applicationDTO.UpdateForm{} + + err := UpdateApplicationForm(suite.ctx, nonApplicationPhaseID, updateForm) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), "course phase is not an application phase", err.Error()) +} + +func TestApplicationAdminServiceTestSuite(t *testing.T) { + suite.Run(t, new(ApplicationAdminServiceTestSuite)) +} diff --git a/server/applicationAdministration/validation_test.go b/server/applicationAdministration/validation_test.go index 9ef3228..a1ab6f9 100644 --- a/server/applicationAdministration/validation_test.go +++ b/server/applicationAdministration/validation_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/suite" ) -type ApplicationAdministrationTestSuite struct { +type ApplicationAdminValidationTestSuite struct { suite.Suite router *gin.Engine ctx context.Context @@ -21,7 +21,7 @@ type ApplicationAdministrationTestSuite struct { applicationAdminService ApplicationService } -func (suite *ApplicationAdministrationTestSuite) SetupSuite() { +func (suite *ApplicationAdminValidationTestSuite) SetupSuite() { suite.ctx = context.Background() // Set up PostgreSQL container @@ -40,11 +40,11 @@ func (suite *ApplicationAdministrationTestSuite) SetupSuite() { suite.router = gin.Default() } -func (suite *ApplicationAdministrationTestSuite) TearDownSuite() { +func (suite *ApplicationAdminValidationTestSuite) TearDownSuite() { suite.cleanup() } -func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_Success() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateUpdateForm_Success() { coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") updateForm := applicationDTO.UpdateForm{ DeleteQuestionsText: []uuid.UUID{uuid.MustParse("a6a04042-95d1-4765-8592-caf9560c8c3c")}, @@ -70,7 +70,7 @@ func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_Success( assert.NoError(suite.T(), err) } -func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidDeleteQuestion() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateUpdateForm_InvalidDeleteQuestion() { coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") updateForm := applicationDTO.UpdateForm{ DeleteQuestionsText: []uuid.UUID{uuid.New()}, // Non-existent question @@ -80,7 +80,7 @@ func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidD assert.Equal(suite.T(), "question does not belong to this course", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidCreateTextQuestion() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateUpdateForm_InvalidCreateTextQuestion() { coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") updateForm := applicationDTO.UpdateForm{ CreateQuestionsText: []applicationDTO.CreateQuestionText{ @@ -96,7 +96,7 @@ func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidC assert.Equal(suite.T(), "course phase id is not correct", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidCreateMultiSelect() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateUpdateForm_InvalidCreateMultiSelect() { coursePhaseID := uuid.MustParse("4179d58a-d00d-4fa7-94a5-397bc69fab02") updateForm := applicationDTO.UpdateForm{ CreateQuestionsMultiSelect: []applicationDTO.CreateQuestionMultiSelect{ @@ -114,42 +114,42 @@ func (suite *ApplicationAdministrationTestSuite) TestValidateUpdateForm_InvalidC assert.Equal(suite.T(), "minimum selection must be at least 0", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionText_EmptyTitle() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionText_EmptyTitle() { err := validateQuestionText("", "", 100) assert.Error(suite.T(), err) assert.Equal(suite.T(), "title is required", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionText_InvalidRegex() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionText_InvalidRegex() { err := validateQuestionText("Valid Title", "[", 100) assert.Error(suite.T(), err) assert.Contains(suite.T(), err.Error(), "invalid regex pattern") } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_EmptyTitle() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionMultiSelect_EmptyTitle() { err := validateQuestionMultiSelect("", 1, 3, []string{"Option1", "Option2"}) assert.Error(suite.T(), err) assert.Equal(suite.T(), "title is required", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_EmptyOptions() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionMultiSelect_EmptyOptions() { err := validateQuestionMultiSelect("Valid Title", 1, 3, []string{}) assert.Error(suite.T(), err) assert.Equal(suite.T(), "options cannot be empty", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_MinSelectNegative() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionMultiSelect_MinSelectNegative() { err := validateQuestionMultiSelect("Valid Title", -1, 3, []string{"Option1", "Option2"}) assert.Error(suite.T(), err) assert.Equal(suite.T(), "minimum selection must be at least 0", err.Error()) } -func (suite *ApplicationAdministrationTestSuite) TestValidateQuestionMultiSelect_MaxSelectLessThanOne() { +func (suite *ApplicationAdminValidationTestSuite) TestValidateQuestionMultiSelect_MaxSelectLessThanOne() { err := validateQuestionMultiSelect("Valid Title", 0, 0, []string{"Option1", "Option2"}) assert.Error(suite.T(), err) assert.Equal(suite.T(), "maximum selection must be at least 1", err.Error()) } func TestValidateUpdateFormSuite(t *testing.T) { - suite.Run(t, new(ApplicationAdministrationTestSuite)) + suite.Run(t, new(ApplicationAdminValidationTestSuite)) } From 2591f0a8ee511c5e98be2eb8e8bcc2f683499383 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 09:48:39 +0100 Subject: [PATCH 3/7] adding router tests --- server/applicationAdministration/router.go | 3 - .../applicationAdministration/router_test.go | 147 ++++++++++++++++++ server/applicationAdministration/service.go | 2 - 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 server/applicationAdministration/router_test.go diff --git a/server/applicationAdministration/router.go b/server/applicationAdministration/router.go index 9d35137..42075b9 100644 --- a/server/applicationAdministration/router.go +++ b/server/applicationAdministration/router.go @@ -20,9 +20,6 @@ func setupApplicationRouter(router *gin.RouterGroup, authMiddleware func() gin.H application.GET("/:coursePhaseID/form", permissionIDMiddleware(keycloak.CourseLecturer, keycloak.CourseEditor), getApplicationForm) application.PUT("/:coursePhaseID/form", permissionIDMiddleware(keycloak.CourseLecturer), updateApplicationForm) - // Application Endpoints - // application.GET("/:coursePhaseID/applications", permissionIDMiddleware("CourseLecturer", "CourseEditor"), getApplications) - } func getApplicationForm(c *gin.Context) { diff --git a/server/applicationAdministration/router_test.go b/server/applicationAdministration/router_test.go new file mode 100644 index 0000000..eac5866 --- /dev/null +++ b/server/applicationAdministration/router_test.go @@ -0,0 +1,147 @@ +package applicationAdministration + +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/applicationAdministration/applicationDTO" + "github.com/niclasheun/prompt2.0/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type ApplicationAdminRouterTestSuite struct { + suite.Suite + router *gin.Engine + ctx context.Context + cleanup func() + applicationAdminService ApplicationService +} + +func (suite *ApplicationAdminRouterTestSuite) SetupSuite() { + suite.ctx = context.Background() + + // Set up PostgreSQL container + testDB, cleanup, err := testutils.SetupTestDB(suite.ctx, "../database_dumps/application_administration.sql") + if err != nil { + log.Fatalf("Failed to set up test database: %v", err) + } + + suite.cleanup = cleanup + suite.applicationAdminService = ApplicationService{ + queries: *testDB.Queries, + conn: testDB.Conn, + } + + ApplicationServiceSingleton = &suite.applicationAdminService + suite.router = gin.Default() + api := suite.router.Group("/api") + setupApplicationRouter(api, func() gin.HandlerFunc { + return testutils.MockAuthMiddleware([]string{"PROMPT_Admin", "iPraktikum-ios24245-Lecturer"}) + }, testutils.MockPermissionMiddleware) + +} + +func (suite *ApplicationAdminRouterTestSuite) TearDownSuite() { + suite.cleanup() +} + +func (suite *ApplicationAdminRouterTestSuite) TestGetApplicationFormEndpoint_Success() { + coursePhaseID := "4179d58a-d00d-4fa7-94a5-397bc69fab02" + req := httptest.NewRequest(http.MethodGet, "/api/applications/"+coursePhaseID+"/form", nil) + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + var form applicationDTO.Form + err := json.Unmarshal(resp.Body.Bytes(), &form) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), form) + assert.NotEmpty(suite.T(), form.QuestionsText) + assert.NotEmpty(suite.T(), form.QuestionsMultiSelect) +} + +func (suite *ApplicationAdminRouterTestSuite) TestUpdateApplicationFormEndpoint_Success() { + coursePhaseID := "4179d58a-d00d-4fa7-94a5-397bc69fab02" + updateForm := applicationDTO.UpdateForm{ + DeleteQuestionsText: []uuid.UUID{uuid.MustParse("a6a04042-95d1-4765-8592-caf9560c8c3c")}, + DeleteQuestionsMultiSelect: []uuid.UUID{uuid.MustParse("65e25b73-ce47-4536-b651-a1632347d733")}, + CreateQuestionsText: []applicationDTO.CreateQuestionText{ + { + CoursePhaseID: uuid.MustParse(coursePhaseID), + Title: "New Motivation", + AllowedLength: 300, + }, + }, + CreateQuestionsMultiSelect: []applicationDTO.CreateQuestionMultiSelect{ + { + CoursePhaseID: uuid.MustParse(coursePhaseID), + Title: "New Devices", + MinSelect: 1, + MaxSelect: 5, + Options: []string{"Option1", "Option2"}, + }, + }, + } + + jsonBody, err := json.Marshal(updateForm) + assert.NoError(suite.T(), err) + + req := httptest.NewRequest(http.MethodPut, "/api/applications/"+coursePhaseID+"/form", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + + suite.router.ServeHTTP(resp, req) + + assert.Equal(suite.T(), http.StatusOK, resp.Code) + var responseBody map[string]string + err = json.Unmarshal(resp.Body.Bytes(), &responseBody) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), "application form updated", responseBody["message"]) + + // Verify updates via GET + getReq := httptest.NewRequest(http.MethodGet, "/api/applications/"+coursePhaseID+"/form", nil) + getResp := httptest.NewRecorder() + suite.router.ServeHTTP(getResp, getReq) + + assert.Equal(suite.T(), http.StatusOK, getResp.Code) + var updatedForm applicationDTO.Form + err = json.Unmarshal(getResp.Body.Bytes(), &updatedForm) + assert.NoError(suite.T(), err) + + // Verify QuestionsText + assert.Equal(suite.T(), 2, len(updatedForm.QuestionsText)) + for _, question := range updatedForm.QuestionsText { + if question.Title == "New Motivation" { + assert.Equal(suite.T(), 300, question.AllowedLength) + } else if question.Title == "Expierence" { + assert.Equal(suite.T(), 500, question.AllowedLength) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } + + // Verify QuestionsMultiSelect + assert.Equal(suite.T(), 2, len(updatedForm.QuestionsMultiSelect)) + for _, question := range updatedForm.QuestionsMultiSelect { + if question.Title == "New Devices" { + assert.ElementsMatch(suite.T(), []string{"Option1", "Option2"}, question.Options) + } else if question.Title == "Available Devices" { + assert.ElementsMatch(suite.T(), []string{"iPhone", "iPad", "MacBook", "Vision"}, question.Options) + } else { + suite.T().Errorf("Unexpected question title: %s", question.Title) + } + } +} + +func TestApplicationAdminRouterTestSuite(t *testing.T) { + suite.Run(t, new(ApplicationAdminRouterTestSuite)) +} diff --git a/server/applicationAdministration/service.go b/server/applicationAdministration/service.go index 1b4e23e..a36ff53 100644 --- a/server/applicationAdministration/service.go +++ b/server/applicationAdministration/service.go @@ -9,7 +9,6 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/niclasheun/prompt2.0/applicationAdministration/applicationDTO" db "github.com/niclasheun/prompt2.0/db/sqlc" - "github.com/sirupsen/logrus" ) type ApplicationService struct { @@ -64,7 +63,6 @@ func UpdateApplicationForm(ctx context.Context, coursePhaseId uuid.UUID, form ap // Delete all questions to be deleted for _, questionID := range form.DeleteQuestionsMultiSelect { - logrus.Info("questionID", questionID) err := ApplicationServiceSingleton.queries.DeleteApplicationQuestionMultiSelect(ctx, questionID) if err != nil { return errors.New("could not delete question") From 110146495e0cf9b2a6c4f048146f2a83d3859a66 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 09:52:17 +0100 Subject: [PATCH 4/7] increasing timeout to make them less flakey --- server/testutils/db_setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/testutils/db_setup.go b/server/testutils/db_setup.go index c94db72..57e65b1 100644 --- a/server/testutils/db_setup.go +++ b/server/testutils/db_setup.go @@ -27,7 +27,7 @@ func SetupTestDB(ctx context.Context, sqlDumpPath string) (*TestDB, func(), erro "POSTGRES_PASSWORD": "testpass", "POSTGRES_DB": "prompt", }, - WaitingFor: wait.ForListeningPort("5432/tcp").WithStartupTimeout(60 * time.Second), + WaitingFor: wait.ForListeningPort("5432/tcp").WithStartupTimeout(120 * time.Second), } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ From d6838ecb98635494ea7013e01e64dc119b721038 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 10:04:17 +0100 Subject: [PATCH 5/7] trying to rewrite server tests --- .github/workflows/go-tests.yml | 39 ++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index 206a322..df3176c 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -7,24 +7,41 @@ on: jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - directory: [ 'server' ] steps: - - uses: actions/checkout@v4 + - name: Checkout Code + 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: cd ${{ matrix.directory }} && go test ./... -json > TestResults-${{ matrix.directory }}.json - - name: Upload Go test results + + - name: Find Testable Folders + id: find_folders + run: | + testable_folders=$(find . -type f -name '*_test.go' -exec dirname {} \; | sort -u | jq -R -s -c 'split("\n")[:-1]') + echo "Testable folders: $testable_folders" + echo "::set-output name=folders::$testable_folders" + + - name: Run Tests in Each Folder + run: | + folders=$(echo "${{ steps.find_folders.outputs.folders }}" | jq -r '.[]') + for folder in $folders; do + echo "Running tests in $folder" + if ls $folder/*_test.go > /dev/null 2>&1; then + cd $folder + go mod download + go test ./... -json > TestResults.json || true + mv TestResults.json ../TestResults-${folder//\//_}.json + cd - + fi + done + + - name: Upload Test Results if: always() uses: actions/upload-artifact@v4 with: - name: Go-results-${{ matrix.directory }} - path: ./${{ matrix.directory }}/TestResults-${{ matrix.directory }}.json \ No newline at end of file + name: Test-Results + path: TestResults-*.json From 0f4f905e1281e53098f57cfe307cf480877f464f Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 10:11:02 +0100 Subject: [PATCH 6/7] trying different testing strategy --- .github/workflows/go-tests.yml | 39 ++++++++++++---------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index df3176c..4cc9c37 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -7,41 +7,30 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + directory: [ 'server' ] steps: - - name: Checkout Code - uses: actions/checkout@v4 - + - uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.22 cache-dependency-path: "**/*.sum" - - - name: Find Testable Folders - id: find_folders + - name: Install dependencies + run: cd ${{ matrix.directory }} && go mod download + - name: Find and Test Go Files run: | - testable_folders=$(find . -type f -name '*_test.go' -exec dirname {} \; | sort -u | jq -R -s -c 'split("\n")[:-1]') - echo "Testable folders: $testable_folders" - echo "::set-output name=folders::$testable_folders" - - - name: Run Tests in Each Folder - run: | - folders=$(echo "${{ steps.find_folders.outputs.folders }}" | jq -r '.[]') - for folder in $folders; do - echo "Running tests in $folder" - if ls $folder/*_test.go > /dev/null 2>&1; then - cd $folder - go mod download - go test ./... -json > TestResults.json || true - mv TestResults.json ../TestResults-${folder//\//_}.json - cd - + find ${{ matrix.directory }} -type d | while read -r dir; do + if ls "$dir"/*.go > /dev/null 2>&1; then + echo "Running tests in $dir" + go test "$dir" -json >> TestResults-${{ matrix.directory }}.json fi done - - - name: Upload Test Results + - name: Upload Go test results if: always() uses: actions/upload-artifact@v4 with: - name: Test-Results - path: TestResults-*.json + name: Go-results-${{ matrix.directory }} + path: ./${{ matrix.directory }}/TestResults-${{ matrix.directory }}.json From 15b087183510b0936696630fd71162f546aab334 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Tue, 24 Dec 2024 10:16:53 +0100 Subject: [PATCH 7/7] works on my machine --- .github/workflows/go-tests.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index 4cc9c37..206a322 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -20,17 +20,11 @@ jobs: cache-dependency-path: "**/*.sum" - name: Install dependencies run: cd ${{ matrix.directory }} && go mod download - - name: Find and Test Go Files - run: | - find ${{ matrix.directory }} -type d | while read -r dir; do - if ls "$dir"/*.go > /dev/null 2>&1; then - echo "Running tests in $dir" - go test "$dir" -json >> TestResults-${{ matrix.directory }}.json - fi - done + - name: Test with Go + run: cd ${{ matrix.directory }} && go test ./... -json > TestResults-${{ matrix.directory }}.json - name: Upload Go test results if: always() uses: actions/upload-artifact@v4 with: name: Go-results-${{ matrix.directory }} - path: ./${{ matrix.directory }}/TestResults-${{ matrix.directory }}.json + path: ./${{ matrix.directory }}/TestResults-${{ matrix.directory }}.json \ No newline at end of file