diff --git a/tests/main_test.go b/tests/main_test.go index 5373139..5131afc 100644 --- a/tests/main_test.go +++ b/tests/main_test.go @@ -2,6 +2,7 @@ package tests import ( "bytes" + "database/sql" "encoding/json" "fmt" "gitea.pena/SQuiz/common/model" @@ -22,9 +23,9 @@ import ( // todo нужно определить из кликхауса на чем будем тестировать статистику var PublicKey = `-----BEGIN PUBLIC KEY-----MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAE=-----END PUBLIC KEY-----` - -var baseURL = "http://127.0.0.1:1488" //os.Getenv("API_BASE_URL") -var validToken = CreateJWT(validUserID) // validUserID +var postgresCred = "postgres://squiz:Redalert2@localhost:35432/squiz?sslmode=disable" //os.Getenv("POSTGRES_CRED") +var baseURL = "http://127.0.0.1:1488" //os.Getenv("API_BASE_URL") +var validToken = CreateJWT(validUserID) // validUserID var expiredToken = CreateExpiredToken(validUserID) // todo @@ -7250,6 +7251,221 @@ func TestCreateQuizTemplate_SpecialCases(t *testing.T) { }) } +// todo перевроверить через дамп +type TestData struct { + Quiz model.Quiz + Questions []model.Question + Answers []model.Answer +} + +func createTestDataForResults(t *testing.T, token string, quizName string) *TestData { + quizResp, err := createQuizRequest(token, map[string]interface{}{ + "name": quizName, + "status": "start", + }) + assert.NoError(t, err) + defer quizResp.Body.Close() + assert.Equal(t, http.StatusCreated, quizResp.StatusCode) + + var quizResult model.Quiz + err = json.NewDecoder(quizResp.Body).Decode(&quizResult) + assert.NoError(t, err) + + questions := []model.Question{} + questionData := []map[string]interface{}{ + { + "title": "Какой основной компонент воздуха?", + "type": "variant", + "description": "Выберите один правильный ответ.", + "required": true, + "page": 1, + "content": `{"variants": ["Кислород", "Азот", "Углекислый газ", "Водород"], "correct": 1}`, + }, + { + "title": "Столица России?", + "type": "text", + "description": "Введите название столицы.", + "required": true, + "page": 1, + "content": `{"maxLength": 50}`, + }, + { + "title": "Ваш возраст", + "type": "number", + "description": "Укажите ваш возраст.", + "required": false, + "page": 2, + "content": `{"min": 0, "max": 120}`, + }, + { + "title": "Форма контактов", + "type": "result", + "description": "Оставьте свои контактные данные.", + "required": true, + "page": 2, + "content": `{"fields": ["name", "email", "phone"]}`, + }, + } + + for _, qData := range questionData { + qData["quiz_id"] = quizResult.Id + questionResp, err := createQuestionRequest(token, qData) + assert.NoError(t, err) + defer questionResp.Body.Close() + assert.Equal(t, http.StatusOK, questionResp.StatusCode) + + var questionResult model.Question + err = json.NewDecoder(questionResp.Body).Decode(&questionResult) + assert.NoError(t, err) + questions = append(questions, questionResult) + } + + answers := createAnswersInDB(t, quizResult.Id, questions) + + return &TestData{ + Quiz: quizResult, + Questions: questions, + Answers: answers, + } +} + +func createAnswersInDB(t *testing.T, quizID uint64, questions []model.Question) []model.Answer { + db, err := sql.Open("postgres", postgresCred) + assert.NoError(t, err) + defer db.Close() + + answers := []model.Answer{} + + sessions := []string{faker.String(), faker.String(), faker.String()} + + for i, session := range sessions { + for j, question := range questions { + if question.Type == "result" { + continue + } + + answerContent := "" + switch question.Type { + case "variant": + answerContent = `{"selected": 1, "answer": "Азот"}` + case "text": + answerContent = `{"answer": "Москва"}` + case "number": + answerContent = `{"answer": 25}` + default: + answerContent = `{"answer": "тестовый ответ"}` + } + + query := ` + INSERT INTO answer (content, quiz_id, question_id, fingerprint, session, result, new, deleted, email, device_type, device, browser, ip, os, start, version) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + RETURNING id, content, quiz_id, question_id, fingerprint, session, result, new, deleted, email, device_type, device, browser, ip, os, start, version, created_at + ` + + var answer model.Answer + err := db.QueryRow( + query, + answerContent, + quizID, + question.Id, + fmt.Sprintf("fingerprint_%d_%d", i, j), + session, + false, + true, + false, + faker.Email(), + "desktop", + "Chrome", + "Chrome 120.0", + "192.168.1.1", + "Windows 10", + j == 0, + 1, + ).Scan( + &answer.Id, + &answer.Content, + &answer.QuizId, + &answer.QuestionId, + &answer.Fingerprint, + &answer.Session, + &answer.Result, + &answer.New, + &answer.Deleted, + &answer.Email, + &answer.DeviceType, + &answer.Device, + &answer.Browser, + &answer.IP, + &answer.OS, + &answer.Start, + &answer.Version, + &answer.CreatedAt, + ) + + assert.NoError(t, err) + answers = append(answers, answer) + } + + for _, question := range questions { + if question.Type == "result" { + answerContent := `{"name": "Иван Иванов", "email": "ivan@test.com", "phone": "+7-999-123-45-67"}` + + query := ` + INSERT INTO answer (content, quiz_id, question_id, fingerprint, session, result, new, deleted, email, device_type, device, browser, ip, os, start, version) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + RETURNING id, content, quiz_id, question_id, fingerprint, session, result, new, deleted, email, device_type, device, browser, ip, os, start, version, created_at + ` + + var answer model.Answer + err := db.QueryRow( + query, + answerContent, + quizID, + question.Id, + fmt.Sprintf("fingerprint_result_%d", i), + session, + true, + true, + false, + faker.Email(), + "desktop", + "Chrome", + "Chrome 120.0", + "192.168.1.1", + "Windows 10", + false, + 1, + ).Scan( + &answer.Id, + &answer.Content, + &answer.QuizId, + &answer.QuestionId, + &answer.Fingerprint, + &answer.Session, + &answer.Result, + &answer.New, + &answer.Deleted, + &answer.Email, + &answer.DeviceType, + &answer.Device, + &answer.Browser, + &answer.IP, + &answer.OS, + &answer.Start, + &answer.Version, + &answer.CreatedAt, + ) + + assert.NoError(t, err) + answers = append(answers, answer) + break + } + } + } + + return answers +} + func getResultsRequest(token string, quizId string, body map[string]interface{}) (*http.Response, error) { payload, err := json.Marshal(body) if err != nil { @@ -7264,58 +7480,11 @@ func getResultsRequest(token string, quizId string, body map[string]interface{}) return http.DefaultClient.Do(req) } -// todo для этих тестов нужно заранее заполнять бдшку ответами на вопросы +// отсмотрено func TestGetResults_Success(t *testing.T) { - quizResp, err := createQuizRequest(validToken, map[string]interface{}{ - "name": "Квиз для тестирования результатов", - "status": "start", - }) - assert.NoError(t, err) - defer quizResp.Body.Close() + testData := createTestDataForResults(t, validToken, "Квиз для тестирования результатов") - var quizResult model.Quiz - err = json.NewDecoder(quizResp.Body).Decode(&quizResult) - assert.NoError(t, err) - quizID := quizResult.Id - - question1Resp, err := createQuestionRequest(validToken, map[string]interface{}{ - "quiz_id": quizID, - "title": "Какой основной компонент воздуха?", - "type": "variant", - "description": "Выберите один правильный ответ.", - "required": true, - "page": 1, - "content": `{"variants": ["Кислород", "Азот", "Углекислый газ", "Водород"]}`, - }) - assert.NoError(t, err) - defer question1Resp.Body.Close() - - var question1Result model.Question - err = json.NewDecoder(question1Resp.Body).Decode(&question1Result) - assert.NoError(t, err) - - question2Resp, err := createQuestionRequest(validToken, map[string]interface{}{ - "quiz_id": quizID, - "title": "Столица России?", - "type": "text", - "description": "Введите название города.", - "required": true, - "page": 1, - "content": `{"placeholder": "Введите ответ"}`, - }) - assert.NoError(t, err) - defer question2Resp.Body.Close() - - var question2Result model.Question - err = json.NewDecoder(question2Resp.Body).Decode(&question2Result) - assert.NoError(t, err) - - // Создаем тестовые ответы (симулируем прохождение квиза) - // В реальном приложении ответы создаются через отдельный API - // Здесь мы будем тестировать получение результатов без ответов - - // Тестируем получение результатов - resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", testData.Quiz.Id), map[string]interface{}{ "Page": 0, "Limit": 10, }) @@ -7331,18 +7500,14 @@ func TestGetResults_Success(t *testing.T) { assert.NotNil(t, result.TotalCount) assert.NotNil(t, result.Results) - // Проверяем, что результаты возвращаются в правильном формате assert.IsType(t, uint64(0), result.TotalCount) assert.IsType(t, []model.AnswerExport{}, result.Results) - // Если есть результаты, проверяем их структуру - if len(result.Results) > 0 { - for _, answer := range result.Results { - assert.NotEmpty(t, answer.Id) - assert.NotEmpty(t, answer.CreatedAt) - assert.IsType(t, bool(false), answer.New) - assert.IsType(t, int32(0), answer.Version) - } + assert.Greater(t, result.TotalCount, uint64(0)) + assert.Greater(t, len(result.Results), 0) + + for _, answer := range result.Results { + assert.NotEmpty(t, answer.Id) } } @@ -7403,40 +7568,11 @@ func TestGetResults_InputValidation(t *testing.T) { }) } -// todo для этих тестов нужно заранее заполнять бдшку ответами на вопросы func TestGetResults_Pagination(t *testing.T) { - // Создаем квиз для тестирования пагинации - quizResp, err := createQuizRequest(validToken, map[string]interface{}{ - "name": "Квиз для тестирования пагинации результатов", - "status": "start", - }) - assert.NoError(t, err) - defer quizResp.Body.Close() - - var quizResult model.Quiz - err = json.NewDecoder(quizResp.Body).Decode(&quizResult) - assert.NoError(t, err) - quizID := quizResult.Id - - // Создаем несколько вопросов для квиза - questionResp, err := createQuestionRequest(validToken, map[string]interface{}{ - "quiz_id": quizID, - "title": "Тестовый вопрос для пагинации", - "type": "text", - "description": "Введите ответ.", - "required": true, - "page": 1, - "content": `{"placeholder": "Введите ответ"}`, - }) - assert.NoError(t, err) - defer questionResp.Body.Close() - - var questionResult model.Question - err = json.NewDecoder(questionResp.Body).Decode(&questionResult) - assert.NoError(t, err) + testData := createTestDataForResults(t, validToken, "Квиз для тестирования пагинации результатов") t.Run("FirstPage", func(t *testing.T) { - resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", testData.Quiz.Id), map[string]interface{}{ "Page": 0, "Limit": 5, }) @@ -7448,13 +7584,12 @@ func TestGetResults_Pagination(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&result) assert.NoError(t, err) - // Проверяем, что количество результатов не превышает лимит assert.LessOrEqual(t, uint64(len(result.Results)), uint64(5)) assert.Equal(t, result.TotalCount, uint64(len(result.Results))) }) t.Run("SecondPage", func(t *testing.T) { - resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", testData.Quiz.Id), map[string]interface{}{ "Page": 1, "Limit": 5, }) @@ -7472,7 +7607,7 @@ func TestGetResults_Pagination(t *testing.T) { }) t.Run("EmptyPage", func(t *testing.T) { - resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", testData.Quiz.Id), map[string]interface{}{ "Page": 100, "Limit": 5, }) @@ -7490,7 +7625,7 @@ func TestGetResults_Pagination(t *testing.T) { }) t.Run("ZeroLimit", func(t *testing.T) { - resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", quizID), map[string]interface{}{ + resp, err := getResultsRequest(validToken, fmt.Sprintf("%v", testData.Quiz.Id), map[string]interface{}{ "Page": 0, "Limit": 0, }) diff --git a/tests/schema/000001_init.down.sql b/tests/schema/000001_init.down.sql new file mode 100644 index 0000000..54966a0 --- /dev/null +++ b/tests/schema/000001_init.down.sql @@ -0,0 +1,24 @@ +-- Drop indexes +DROP INDEX IF EXISTS subquizes; +DROP INDEX IF EXISTS birthtime; +DROP INDEX IF EXISTS groups; +DROP INDEX IF EXISTS timeouted; +DROP INDEX IF EXISTS active ON quiz; +DROP INDEX IF EXISTS questiontype; +DROP INDEX IF EXISTS required; +DROP INDEX IF EXISTS relation; +DROP INDEX IF EXISTS active ON question; + +-- Drop tables +DROP TABLE IF EXISTS privileges; +DROP TABLE IF EXISTS answer; +DROP TABLE IF EXISTS question; +DROP TABLE IF EXISTS quiz; +DROP TABLE IF EXISTS account; + +-- Drop types +DO $$ +BEGIN +DROP TYPE IF EXISTS question_type; +DROP TYPE IF EXISTS quiz_status; +END$$; \ No newline at end of file diff --git a/tests/schema/000001_init.up.sql b/tests/schema/000001_init.up.sql new file mode 100644 index 0000000..9aefa67 --- /dev/null +++ b/tests/schema/000001_init.up.sql @@ -0,0 +1,120 @@ +-- Create types +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'question_type') THEN +CREATE TYPE question_type AS ENUM ( + 'variant', + 'images', + 'varimg', + 'emoji', + 'text', + 'select', + 'date', + 'number', + 'file', + 'page', + 'rating' + ); +END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'quiz_status') THEN +CREATE TYPE quiz_status AS ENUM ( + 'draft', + 'template', + 'stop', + 'start', + 'timeout', + 'offlimit' + ); +END IF; + + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +END$$; + +-- Create tables +CREATE TABLE IF NOT EXISTS account ( + id UUID PRIMARY KEY, + user_id VARCHAR(24), + email VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted BOOLEAN DEFAULT false + ); + +CREATE TABLE IF NOT EXISTS quiz ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + qid uuid DEFAULT uuid_generate_v4(), + accountid varchar(30) NOT NULL, + deleted boolean DEFAULT false, + archived boolean DEFAULT false, + fingerprinting boolean DEFAULT false, + repeatable boolean DEFAULT false, + note_prevented boolean DEFAULT false, + mail_notifications boolean DEFAULT false, + unique_answers boolean DEFAULT false, + super boolean DEFAULT false, + group_id bigint DEFAULT 0, + name varchar(280), + description text, + config text, + status quiz_status DEFAULT 'draft', + limit_answers integer DEFAULT 0, + due_to integer DEFAULT 0, + time_of_passing integer DEFAULT 0, + pausable boolean DEFAULT false, + version smallint DEFAULT 0, + version_comment text DEFAULT '', + parent_ids integer[], + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + questions_count integer DEFAULT 0, + answers_count integer DEFAULT 0, + average_time_passing integer DEFAULT 0 + ); + +CREATE TABLE IF NOT EXISTS question ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + quiz_id bigint NOT NULL, + title varchar(512) NOT NULL, + description text, + questiontype question_type DEFAULT 'text', + required boolean DEFAULT false, + deleted boolean DEFAULT false, + page smallint DEFAULT 0, + content text, + version smallint DEFAULT 0, + parent_ids integer[], + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT quiz_relation FOREIGN KEY(quiz_id) REFERENCES quiz(id) + ); + +CREATE TABLE IF NOT EXISTS answer ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + content text, + quiz_id bigint NOT NULL REFERENCES quiz(id), + question_id bigint NOT NULL REFERENCES question(id), + fingerprint varchar(1024), + session varchar(20), + created_at timestamp DEFAULT CURRENT_TIMESTAMP + ); + +CREATE TABLE IF NOT EXISTS privileges ( + id SERIAL PRIMARY KEY, + privilegeID VARCHAR(50), + account_id UUID, + privilege_name VARCHAR(255), + amount INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES account (id) + ); + +-- Create indexes +CREATE INDEX IF NOT EXISTS active ON question(deleted) WHERE deleted=false; +CREATE INDEX IF NOT EXISTS relation ON question(quiz_id DESC); +CREATE INDEX IF NOT EXISTS required ON question(required DESC); +CREATE INDEX IF NOT EXISTS questiontype ON question(questiontype); +CREATE INDEX IF NOT EXISTS active ON quiz(deleted, archived, status) WHERE deleted = false AND archived = false AND status = 'start'; +CREATE INDEX IF NOT EXISTS timeouted ON quiz(due_to DESC) WHERE deleted = false AND due_to <> 0 AND status <> 'timeout'; +CREATE INDEX IF NOT EXISTS groups ON quiz(super) WHERE super = true; +CREATE INDEX IF NOT EXISTS birthtime ON quiz(created_at DESC); +CREATE INDEX IF NOT EXISTS subquizes ON quiz(group_id DESC) WHERE group_id <> 0; diff --git a/tests/schema/000002_init.down.sql b/tests/schema/000002_init.down.sql new file mode 100644 index 0000000..0aa3d9e --- /dev/null +++ b/tests/schema/000002_init.down.sql @@ -0,0 +1 @@ +ALTER TABLE answer DROP COLUMN IF EXISTS result; diff --git a/tests/schema/000002_init.up.sql b/tests/schema/000002_init.up.sql new file mode 100644 index 0000000..4083153 --- /dev/null +++ b/tests/schema/000002_init.up.sql @@ -0,0 +1 @@ +ALTER TABLE answer ADD COLUMN result BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/tests/schema/000003_init.down.sql b/tests/schema/000003_init.down.sql new file mode 100644 index 0000000..94e352f --- /dev/null +++ b/tests/schema/000003_init.down.sql @@ -0,0 +1 @@ +ALTER TABLE quiz DROP COLUMN IF EXISTS sessions_count; diff --git a/tests/schema/000003_init.up.sql b/tests/schema/000003_init.up.sql new file mode 100644 index 0000000..a292d62 --- /dev/null +++ b/tests/schema/000003_init.up.sql @@ -0,0 +1 @@ +ALTER TABLE quiz ADD COLUMN sessions_count integer; diff --git a/tests/schema/000004_init.down.sql b/tests/schema/000004_init.down.sql new file mode 100644 index 0000000..b6d5ec4 --- /dev/null +++ b/tests/schema/000004_init.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE quiz DROP COLUMN IF EXISTS new; +ALTER TABLE quiz DROP COLUMN IF EXISTS deleted; \ No newline at end of file diff --git a/tests/schema/000004_init.up.sql b/tests/schema/000004_init.up.sql new file mode 100644 index 0000000..9dc5591 --- /dev/null +++ b/tests/schema/000004_init.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer ADD COLUMN new BOOLEAN DEFAULT TRUE; +ALTER TABLE answer ADD COLUMN deleted BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/tests/schema/000005_init.down.sql b/tests/schema/000005_init.down.sql new file mode 100644 index 0000000..ccae4f5 --- /dev/null +++ b/tests/schema/000005_init.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer DROP COLUMN IF EXISTS email; +DROP INDEX IF EXISTS answer_email_unique_idx; \ No newline at end of file diff --git a/tests/schema/000005_init.up.sql b/tests/schema/000005_init.up.sql new file mode 100644 index 0000000..f2bfe97 --- /dev/null +++ b/tests/schema/000005_init.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer ADD COLUMN email VARCHAR(50) NOT NULL DEFAULT ''; +CREATE UNIQUE INDEX IF NOT EXISTS answer_email_unique_idx ON answer (quiz_id, email) WHERE email <> ''; \ No newline at end of file diff --git a/tests/schema/000006_init.down.sql b/tests/schema/000006_init.down.sql new file mode 100644 index 0000000..355ba3b --- /dev/null +++ b/tests/schema/000006_init.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE answer +DROP COLUMN device_type, +DROP COLUMN device, +DROP COLUMN os, +DROP COLUMN browser, +DROP COLUMN ip; \ No newline at end of file diff --git a/tests/schema/000006_init.up.sql b/tests/schema/000006_init.up.sql new file mode 100644 index 0000000..4dc1487 --- /dev/null +++ b/tests/schema/000006_init.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE answer +ADD COLUMN device_type VARCHAR(50) NOT NULL DEFAULT '', +ADD COLUMN device VARCHAR(100) NOT NULL DEFAULT '', +ADD COLUMN os VARCHAR(100) NOT NULL DEFAULT '', +ADD COLUMN browser VARCHAR(100) NOT NULL DEFAULT '', +ADD COLUMN ip VARCHAR(50) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/tests/schema/000007_init.down.sql b/tests/schema/000007_init.down.sql new file mode 100644 index 0000000..374c55d --- /dev/null +++ b/tests/schema/000007_init.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer +DROP COLUMN start; \ No newline at end of file diff --git a/tests/schema/000007_init.up.sql b/tests/schema/000007_init.up.sql new file mode 100644 index 0000000..6b41425 --- /dev/null +++ b/tests/schema/000007_init.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer +ADD COLUMN start BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/tests/schema/000008_init.down.sql b/tests/schema/000008_init.down.sql new file mode 100644 index 0000000..4f06fcb --- /dev/null +++ b/tests/schema/000008_init.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE answer +ALTER COLUMN device TYPE VARCHAR(100) NOT NULL DEFAULT '', +ALTER COLUMN os TYPE VARCHAR(100) NOT NULL DEFAULT '', +ALTER COLUMN browser TYPE VARCHAR(100) NOT NULL DEFAULT '', diff --git a/tests/schema/000008_init.up.sql b/tests/schema/000008_init.up.sql new file mode 100644 index 0000000..fcba321 --- /dev/null +++ b/tests/schema/000008_init.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE answer +ALTER COLUMN device TYPE VARCHAR(1024), +ALTER COLUMN os TYPE VARCHAR(1024), +ALTER COLUMN browser TYPE VARCHAR(1024); diff --git a/tests/schema/000009_init.down.sql b/tests/schema/000009_init.down.sql new file mode 100644 index 0000000..34fb3c6 --- /dev/null +++ b/tests/schema/000009_init.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE answer +ALTER COLUMN device VARCHAR(100) NOT NULL DEFAULT '', +ALTER COLUMN os VARCHAR(100) NOT NULL DEFAULT '', +ALTER COLUMN browser VARCHAR(100) NOT NULL DEFAULT '', diff --git a/tests/schema/000009_init.up.sql b/tests/schema/000009_init.up.sql new file mode 100644 index 0000000..d0c81cf --- /dev/null +++ b/tests/schema/000009_init.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE answer +ALTER COLUMN device TYPE VARCHAR(512), +ALTER COLUMN os TYPE VARCHAR(512), +ALTER COLUMN browser TYPE VARCHAR(512); diff --git a/tests/schema/000010_init.down.sql b/tests/schema/000010_init.down.sql new file mode 100644 index 0000000..02f12e4 --- /dev/null +++ b/tests/schema/000010_init.down.sql @@ -0,0 +1,31 @@ +DROP INDEX IF EXISTS idx_unique_users; +DROP INDEX IF EXISTS idx_unique_pipeline; +DROP INDEX IF EXISTS idx_unique_step; +DROP INDEX IF EXISTS idx_unique_field; +DROP INDEX IF EXISTS idx_unique_tag; +DROP INDEX IF EXISTS idx_unique_rules; + +DROP TABLE IF EXISTS amoCRMStatuses; +DROP TABLE IF EXISTS rules; +DROP TABLE IF EXISTS utms; +DROP TABLE IF EXISTS tags; +DROP TABLE IF EXISTS fields; + +DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'entitytype') THEN + DROP TYPE EntityType; + END IF; +END $$; + +DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'fieldtype') THEN + DROP TYPE FieldType; + END IF; +END $$; + +DROP TABLE IF EXISTS steps; +DROP TABLE IF EXISTS pipelines; +DROP TABLE IF EXISTS tokens; +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/tests/schema/000010_init.up.sql b/tests/schema/000010_init.up.sql new file mode 100644 index 0000000..37ee29e --- /dev/null +++ b/tests/schema/000010_init.up.sql @@ -0,0 +1,119 @@ +CREATE TABLE IF NOT EXISTS tokens ( + AccountID VARCHAR(30) PRIMARY KEY, -- связь с users AccountID неявная посредством join + RefreshToken TEXT NOT NULL , + AccessToken TEXT NOT NULL , + AuthCode TEXT NOT NULL , -- код авторизации который получаем при вебхук + Expiration TIMESTAMP NOT NULL, -- время истечения токенов + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS users ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AccountID VARCHAR(30) NOT NULL DEFAULT '', -- id квизе из токена + AmoID INT NOT NULL , -- id в амо + Name VARCHAR(512) NOT NULL DEFAULT '', -- имя в амо + Email VARCHAR(50) NOT NULL DEFAULT '', -- почта в амо + Role INT NOT NULL DEFAULT 0, -- роль в амо + "Group" INT NOT NULL DEFAULT 0, -- вложенная структура так как в амо группы хранятся массивом структур + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + Subdomain VARCHAR(50) NOT NULL DEFAULT '', + AmoUserID INT NOT NULL , -- id пользователя который подключал интеграцию + Country VARCHAR(50) NOT NULL DEFAULT '' -- страна в амо +); + +CREATE TABLE IF NOT EXISTS pipelines ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoID INT NOT NULL , --id воронки в амо + AccountID INT NOT NULL , --id аккаунта в амо связь с таблицей users AmoID неявная посредством join + Name VARCHAR(512) NOT NULL DEFAULT '', --название воронки в амо + IsArchive BOOLEAN NOT NULL DEFAULT FALSE, --флаг архивной воронки в амо + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS steps ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoID INT NOT NULL, --id шага воронки в амо + PipelineID INT NOT NULL, --id воронки AmoID pipelines неявная посредством join + AccountID INT NOT NULL, --id аккаунта в амо связь с таблицей users AmoID неявная посредством join + Name VARCHAR(512) NOT NULL DEFAULT '', --название воронки в амо + Color VARCHAR(50) NOT NULL DEFAULT '', --цвет шага в амо* + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'entitytype') THEN + CREATE TYPE EntityType AS ENUM ('leads', 'contacts', 'companies', 'customers'); + END IF; +END $$; + +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'fieldtype') THEN + CREATE TYPE FieldType AS ENUM ('text', 'numeric', 'checkbox', 'select', 'multiselect', 'date', 'url', 'textarea', 'radiobutton', 'streetaddress', 'smart_address', 'birthday', 'legal_entity', 'date_time', 'price', 'category', 'items', 'tracking_data', 'linked_entity', 'chained_list', 'monetary', 'file', 'payer', 'supplier', 'multitext'); + END IF; +END $$; + +CREATE TABLE IF NOT EXISTS fields ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoID INT NOT NULL, -- айдишник кастомного поля в амо + Code VARCHAR(255) NOT NULL DEFAULT '', -- кодовое слово в амо + AccountID INT NOT NULL, -- id аккаунта в амо связь с таблицей users AmoID неявная посредством join + Name VARCHAR(512) NOT NULL DEFAULT '', -- название воронки в амо + Entity EntityType NOT NULL, -- тип сущности в амо, для которой это кастомное поле + Type FieldType NOT NULL, -- тип поля + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS tags ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoID INT NOT NULL, -- айдишник тега в амо + AccountID INT NOT NULL, -- id аккаунта в амо связь с таблицей users AmoID неявная посредством join + Entity EntityType NOT NULL, -- сущность, к которой принадлежит этот тег + Name VARCHAR(512) NOT NULL DEFAULT '', -- название тега в амо + Color VARCHAR(50) NOT NULL DEFAULT '', -- цвет тега в амо + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS utms ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoFieldID INT NOT NULL DEFAULT 0, -- id field в амо + QuizID INT NOT NULL, -- id опроса + AccountID INT NOT NULL, -- id аккаунта в амо AMOID + Name VARCHAR(512) NOT NULL DEFAULT '', -- название utm + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS rules ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AccountID INT NOT NULL, -- id аккаунта в амо AMOID + QuizID INT NOT NULL, -- id опроса + PerformerID INT NOT NULL, -- айдишник ответственного за сделку + PipelineID INT NOT NULL, --id воронки AmoID pipelines неявная посредством join + StepID INT NOT NULL , -- id этапа steps AmoID join + UTMS INTEGER[], -- список UTM для этого опроса id utm + FieldsRule JSONB NOT NULL DEFAULT '{}', -- вложенная структура + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX idx_unique_users ON users (amoID); +CREATE UNIQUE INDEX idx_unique_pipeline ON pipelines (amoID, accountID); +CREATE UNIQUE INDEX idx_unique_step ON steps (amoID, accountID, PipelineID); +CREATE UNIQUE INDEX idx_unique_field ON fields (amoID, accountID, entity); +CREATE UNIQUE INDEX idx_unique_tag ON tags (amoID, accountID, entity); +CREATE UNIQUE INDEX idx_unique_rules ON rules (accountID, QuizID); + +CREATE TABLE IF NOT EXISTS amoCRMStatuses ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AccountID INT NOT NULL, -- id аккаунта в амо + DealID INT NOT NULL, -- id сделки в которую добавлялось + AnswerID BIGINT NOT NULL REFERENCES answer(id), -- id true result который вызвал действие + Status TEXT NOT NULL DEFAULT '', -- запись о ошибке, либо успехе + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/tests/schema/000011_init.down.sql b/tests/schema/000011_init.down.sql new file mode 100644 index 0000000..bcaa9ff --- /dev/null +++ b/tests/schema/000011_init.down.sql @@ -0,0 +1,15 @@ +ALTER TABLE answer +DROP COLUMN utm; + +CREATE TABLE IF NOT EXISTS utms ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoFieldID INT NOT NULL DEFAULT 0, -- id field в амо + QuizID INT NOT NULL, -- id опроса + AccountID INT NOT NULL, -- id аккаунта в амо AMOID + Name VARCHAR(512) NOT NULL DEFAULT '', -- название utm + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE rules + ADD COLUMN UTMS INTEGER[]; \ No newline at end of file diff --git a/tests/schema/000011_init.up.sql b/tests/schema/000011_init.up.sql new file mode 100644 index 0000000..f268036 --- /dev/null +++ b/tests/schema/000011_init.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE answer +ADD COLUMN utm jsonb NOT NULL DEFAULT '{}'; +DROP TABLE IF EXISTS utms; +ALTER TABLE rules +DROP COLUMN IF EXISTS UTMS; \ No newline at end of file diff --git a/tests/schema/000012_init.down.sql b/tests/schema/000012_init.down.sql new file mode 100644 index 0000000..0f34183 --- /dev/null +++ b/tests/schema/000012_init.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_unique_users; +DROP INDEX IF EXISTS idx_unique_rules; +CREATE UNIQUE INDEX idx_unique_users ON users (amoID); +CREATE UNIQUE INDEX idx_unique_rules ON rules (accountID, QuizID); \ No newline at end of file diff --git a/tests/schema/000012_init.up.sql b/tests/schema/000012_init.up.sql new file mode 100644 index 0000000..87913a8 --- /dev/null +++ b/tests/schema/000012_init.up.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS idx_unique_users; +DROP INDEX IF EXISTS idx_unique_rules; +CREATE UNIQUE INDEX idx_unique_users ON users (amoID) WHERE Deleted = false; +CREATE UNIQUE INDEX idx_unique_rules ON rules (accountID, QuizID) WHERE Deleted = false; \ No newline at end of file diff --git a/tests/schema/000013_init.down.sql b/tests/schema/000013_init.down.sql new file mode 100644 index 0000000..26cb247 --- /dev/null +++ b/tests/schema/000013_init.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE rules +DROP COLUMN IF EXISTS TagsToAdd; +ALTER TABLE users +DROP COLUMN IF EXISTS DriveURL; \ No newline at end of file diff --git a/tests/schema/000013_init.up.sql b/tests/schema/000013_init.up.sql new file mode 100644 index 0000000..44d4155 --- /dev/null +++ b/tests/schema/000013_init.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE rules +ADD COLUMN TagsToAdd JSONB NOT NULL DEFAULT '{}'; +ALTER TABLE users +ADD COLUMN DriveURL text NOT NULL DEFAULT ''; diff --git a/tests/schema/000014_init.down.sql b/tests/schema/000014_init.down.sql new file mode 100644 index 0000000..5ddb16f --- /dev/null +++ b/tests/schema/000014_init.down.sql @@ -0,0 +1,20 @@ +DROP TABLE IF EXISTS usersAmo; +DROP INDEX IF EXISTS idx_unique_accountsAmo; +DROP TABLE IF EXISTS accountsAmo; + +CREATE TABLE IF NOT EXISTS users ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AccountID VARCHAR(30) NOT NULL DEFAULT '', -- id квизе из токена + AmoID INT NOT NULL , -- id в амо + Name VARCHAR(512) NOT NULL DEFAULT '', -- имя в амо + Email VARCHAR(50) NOT NULL DEFAULT '', -- почта в амо + Role INT NOT NULL DEFAULT 0, -- роль в амо + "Group" INT NOT NULL DEFAULT 0, -- вложенная структура так как в амо группы хранятся массивом структур + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + Subdomain VARCHAR(50) NOT NULL DEFAULT '', + AmoUserID INT NOT NULL , -- id пользователя который подключал интеграцию + Country VARCHAR(50) NOT NULL DEFAULT '' -- страна в амо +); + +CREATE UNIQUE INDEX idx_unique_users ON users (amoID) WHERE Deleted = false; \ No newline at end of file diff --git a/tests/schema/000014_init.up.sql b/tests/schema/000014_init.up.sql new file mode 100644 index 0000000..9542bbb --- /dev/null +++ b/tests/schema/000014_init.up.sql @@ -0,0 +1,28 @@ +DROP TABLE IF EXISTS users; +DROP INDEX IF EXISTS idx_unique_users; + +CREATE TABLE IF NOT EXISTS accountsAmo ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AccountID VARCHAR(30) NOT NULL DEFAULT '', -- ID аккаунта у нас + AmoID INT NOT NULL, -- ID "компании" в амо + Name VARCHAR(512) NOT NULL DEFAULT '', + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + Subdomain VARCHAR(50) NOT NULL DEFAULT '', -- поддомен - пример https://penadigitaltech.amocrm.ru + Country VARCHAR(50) NOT NULL DEFAULT '', + DriveURL VARCHAR(255) NOT NULL DEFAULT '' -- URL объктного хранилища +); + +CREATE UNIQUE INDEX idx_unique_accountsAmo ON accountsAmo (amoID) WHERE Deleted = false; + +CREATE TABLE IF NOT EXISTS usersAmo ( + ID BIGSERIAL UNIQUE NOT NULL PRIMARY KEY, + AmoID INT NOT NULL, -- ID компании в амо (внешний ключ) + AmoUserID INT NOT NULL, -- ID пользователя в амо + Name VARCHAR(512) NOT NULL DEFAULT '', + Email VARCHAR(50) NOT NULL DEFAULT '', + Role INT NOT NULL DEFAULT 0, + "Group" INT NOT NULL DEFAULT 0, + Deleted BOOLEAN NOT NULL DEFAULT FALSE, + CreatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/tests/schema/000015_init.down.sql b/tests/schema/000015_init.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/schema/000015_init.up.sql b/tests/schema/000015_init.up.sql new file mode 100644 index 0000000..3985517 --- /dev/null +++ b/tests/schema/000015_init.up.sql @@ -0,0 +1,19 @@ +UPDATE answer +SET content = + CASE + WHEN content ~ '