Skip to content

Commit

Permalink
feat: Delete test block (#132)
Browse files Browse the repository at this point in the history
* feat(db): Create new table to save archives deletion errors

* feat(blocks): Create new endpoint to delete test blocks

* test(blocks): Add test to ensure teachers can delete test blocks
  • Loading branch information
PedroChaparro authored Dec 30, 2023
1 parent f0285eb commit 3ddaa80
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 2 deletions.
68 changes: 68 additions & 0 deletions __tests__/integration/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,71 @@ func TestUpdateTestBlock(t *testing.T) {
c.Equal(newName, block["name"].(string))
c.Equal(firstLanguageUUID, block["language_uuid"].(string))
}

func TestDeleteTestBlock(t *testing.T) {
c := require.New(t)

// Login as a teacher
w, r := PrepareRequest("POST", "/api/v1/session/login", map[string]interface{}{
"email": registeredTeacherEmail,
"password": registeredTeacherPass,
})
router.ServeHTTP(w, r)
cookie := w.Result().Cookies()[0]

// Create a course
courseUUID, status := CreateCourse("Delete test block test - course")
c.Equal(http.StatusCreated, status)

// Create a laboratory
laboratoryCreationResponse, status := CreateLaboratory(cookie, map[string]interface{}{
"name": "Delete test block test - laboratory",
"course_uuid": courseUUID,
"opening_date": "2023-12-01T08:00",
"due_date": "3023-12-01T00:00",
})
c.Equal(http.StatusCreated, status)
laboratoryUUID := laboratoryCreationResponse["uuid"].(string)

// Get the supported languages
languagesResponse, status := GetSupportedLanguages(cookie)
c.Equal(http.StatusOK, status)

languages := languagesResponse["languages"].([]interface{})
c.Greater(len(languages), 0)

firstLanguage := languages[0].(map[string]interface{})
firstLanguageUUID := firstLanguage["uuid"].(string)

// Create a test block
zipFile, err := GetSampleTestsArchive()
c.Nil(err)

blockCreationResponse, status := CreateTestBlock(&CreateTestBlockUtilsDTO{
laboratoryUUID: laboratoryUUID,
languageUUID: firstLanguageUUID,
blockName: "Delete test block test - block",
cookie: cookie,
testFile: zipFile,
})
c.Equal(http.StatusCreated, status)
testBlockUUID := blockCreationResponse["uuid"].(string)

// Check that the test block was created
laboratoryResponse, status := GetLaboratoryByUUID(cookie, laboratoryUUID)
c.Equal(http.StatusOK, status)

blocks := laboratoryResponse["test_blocks"].([]interface{})
c.Equal(1, len(blocks))

// Delete the test block
_, status = DeleteTestBlock(cookie, testBlockUUID)
c.Equal(http.StatusNoContent, status)

// Check that the test block was deleted
laboratoryResponse, status = GetLaboratoryByUUID(cookie, laboratoryUUID)
c.Equal(http.StatusOK, status)

blocks = laboratoryResponse["test_blocks"].([]interface{})
c.Equal(0, len(blocks))
}
10 changes: 10 additions & 0 deletions __tests__/integration/blocks_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,13 @@ func UpdateTestBlock(dto *UpdateTestBlockUtilsDTO) (response map[string]interfac
jsonResponse := ParseJsonResponse(w.Body)
return jsonResponse, w.Code
}

func DeleteTestBlock(cookie *http.Cookie, blockUUID string) (response map[string]interface{}, statusCode int) {
endpoint := fmt.Sprintf("/api/v1/blocks/test_blocks/%s", blockUUID)
w, r := PrepareRequest("DELETE", endpoint, nil)
r.AddCookie(cookie)
router.ServeHTTP(w, r)

jsonResponse := ParseJsonResponse(w.Body)
return jsonResponse, w.Code
}
11 changes: 11 additions & 0 deletions docs/bruno/blocks/delete-test-block.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
meta {
name: delete-test-block
type: http
seq: 3
}

delete {
url: {{BASE_URL}}/blocks/test_blocks/eb4ef0ee-3865-44c3-8d52-2620fb2653e5
body: none
auth: none
}
2 changes: 2 additions & 0 deletions sql/migrations/20230920232901_init.down.sql
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ DROP VIEW IF EXISTS objectives_owners;
DROP VIEW IF EXISTS criteria_owners;

-- ## Tables
DROP TABLE IF EXISTS files_deletion_error_logs;

DROP TABLE IF EXISTS archives;

DROP TABLE IF EXISTS grade_has_criteria;
Expand Down
12 changes: 10 additions & 2 deletions sql/migrations/20230920232901_init.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ CREATE TABLE IF NOT EXISTS archives (
"file_id" UUID NOT NULL UNIQUE
);

CREATE TABLE IF NOT EXISTS files_deletion_error_logs (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
"file_id" UUID NOT NULL REFERENCES archives(id),
"file_type" VARCHAR(16) NOT NULL,
"requested_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"error_message" TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS languages (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
"template_archive_id" UUID NOT NULL UNIQUE REFERENCES archives(id),
Expand All @@ -112,7 +120,7 @@ CREATE TABLE IF NOT EXISTS test_blocks (

CREATE TABLE IF NOT EXISTS submissions (
"id" UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
"test_id" UUID NOT NULL REFERENCES test_blocks(id),
"test_block_id" UUID NOT NULL REFERENCES test_blocks(id) ON DELETE CASCADE,
"student_id" UUID NOT NULL REFERENCES users(id),
"archive_id" UUID NOT NULL UNIQUE REFERENCES archives(id),
"passing" BOOLEAN NOT NULL DEFAULT FALSE,
Expand All @@ -137,7 +145,7 @@ CREATE TABLE IF NOT EXISTS grade_has_criteria (
-- ### Unique indexes
CREATE UNIQUE INDEX IF NOT EXISTS idx_class_users ON courses_has_users(course_id, user_id);

CREATE UNIQUE INDEX IF NOT EXISTS idx_submissions ON submissions(test_id, student_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_submissions ON submissions(test_block_id, student_id);

CREATE UNIQUE INDEX IF NOT EXISTS idx_grades ON grades(laboratory_id, student_id);

Expand Down
15 changes: 15 additions & 0 deletions src/blocks/application/use_cases.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,18 @@ func (useCases *BlocksUseCases) DeleteMarkdownBlock(dto dtos.DeleteBlockDTO) (er
// Delete the block
return useCases.BlocksRepository.DeleteMarkdownBlock(dto.BlockUUID)
}

func (useCases *BlocksUseCases) DeleteTestBlock(dto dtos.DeleteBlockDTO) (err error) {
// Validate the teacher is the owner of the block
ownsBlock, err := useCases.BlocksRepository.DoesTeacherOwnsTestBlock(dto.TeacherUUID, dto.BlockUUID)
if err != nil {
return err
}

if !ownsBlock {
return errors.TeacherDoesNotOwnBlock{}
}

// Delete the block
return useCases.BlocksRepository.DeleteTestBlock(dto.BlockUUID)
}
25 changes: 25 additions & 0 deletions src/blocks/infrastructure/http/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,28 @@ func (controller *BlocksController) HandleDeleteMarkdownBlock(c *gin.Context) {

c.Status(http.StatusNoContent)
}

func (controller *BlocksController) HandleDeleteTestBlock(c *gin.Context) {
teacherUUID := c.GetString("session_uuid")
blockUUID := c.Param("block_uuid")

// Validate the block UUID
if err := sharedInfrastructure.GetValidator().Var(blockUUID, "uuid4"); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"message": "Block UUID is not valid",
})
return
}

// Delete the block
err := controller.UseCases.DeleteTestBlock(dtos.DeleteBlockDTO{
TeacherUUID: teacherUUID,
BlockUUID: blockUUID,
})
if err != nil {
c.Error(err)
return
}

c.Status(http.StatusNoContent)
}
7 changes: 7 additions & 0 deletions src/blocks/infrastructure/http/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ func StartBlocksRoutes(g *gin.RouterGroup) {
sharedInfrastructure.WithAuthorizationMiddleware([]string{"teacher"}),
controller.HandleDeleteMarkdownBlock,
)

blocksGroup.DELETE(
"/test_blocks/:block_uuid",
sharedInfrastructure.WithAuthenticationMiddleware(),
sharedInfrastructure.WithAuthorizationMiddleware([]string{"teacher"}),
controller.HandleDeleteTestBlock,
)
}
124 changes: 124 additions & 0 deletions src/blocks/infrastructure/implementations/blocks_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/textproto"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/UPB-Code-Labs/main-api/src/blocks/domain/dtos"
"github.com/UPB-Code-Labs/main-api/src/blocks/domain/errors"
laboratoriesDomainErrors "github.com/UPB-Code-Labs/main-api/src/laboratories/domain/errors"
sharedEntities "github.com/UPB-Code-Labs/main-api/src/shared/domain/entities"
sharedDomainErrors "github.com/UPB-Code-Labs/main-api/src/shared/domain/errors"
sharedInfrastructure "github.com/UPB-Code-Labs/main-api/src/shared/infrastructure"
)
Expand Down Expand Up @@ -370,6 +372,15 @@ func (repository *BlocksPostgresRepository) DeleteTestBlock(blockUUID string) (e
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

// Get the UUIDs of the dependent archives before deleting the block
dependentArchivesUUIDs, err := repository.getDependentArchivesByTestBlockUUID(blockUUID)
if err != nil {
return err
}

// Delete the dependent archives in a separate goroutine
go repository.deleteDependentArchives(dependentArchivesUUIDs)

// Get the UUID of the block index
query := `
SELECT block_index_id
Expand Down Expand Up @@ -412,3 +423,116 @@ func (repository *BlocksPostgresRepository) deleteBlockIndex(blockIndexUUID stri

return nil
}

func (repository *BlocksPostgresRepository) getDependentArchivesByTestBlockUUID(blockUUID string) (archives []*sharedEntities.StaticFileArchive, err error) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

// Get the UUID of the test block's tests archive
query := `
SELECT file_id
FROM archives
WHERE id = (
SELECT test_archive_id
FROM test_blocks
WHERE id = $1
)
`

row := repository.Connection.QueryRowContext(ctx, query, blockUUID)
var testsArchiveUUID string
if err := row.Scan(&testsArchiveUUID); err != nil {
if err == sql.ErrNoRows {
return nil, &errors.BlockNotFound{}
}

return nil, err
}

archives = append(archives, &sharedEntities.StaticFileArchive{
ArchiveUUID: testsArchiveUUID,
ArchiveType: "test",
})

// Get the UUID of the test block's submissions archives
query = `
SELECT file_id
FROM archives
WHERE id IN (
SELECT archive_id
FROM submissions
WHERE test_block_id = $1
)
`

rows, err := repository.Connection.QueryContext(ctx, query, blockUUID)
if err != nil {
return nil, err
}

for rows.Next() {
var submissionArchiveUUID string
if err := rows.Scan(&submissionArchiveUUID); err != nil {
return nil, err
}

archives = append(archives, &sharedEntities.StaticFileArchive{
ArchiveUUID: submissionArchiveUUID,
ArchiveType: "submission",
})
}

return archives, nil
}

func (repository *BlocksPostgresRepository) deleteDependentArchives(archives []*sharedEntities.StaticFileArchive) {
log.Printf("[INFO] - [BlocksPostgresRepository] - [deleteDependentArchives]: Deleting %d archives \n", len(archives))
staticFilesEndpoint := fmt.Sprintf("%s/archives/delete", sharedInfrastructure.GetEnvironment().StaticFilesMicroserviceAddress)

for _, archive := range archives {
// Create the request body
var body bytes.Buffer
err := json.NewEncoder(&body).Encode(archive)
if err != nil {
errMessage := fmt.Sprintf("[ERR] - [BlocksPostgresRepository] - [deleteDependentArchives]: Unable to encode the request: %s", err.Error())
repository.saveDeletionErrorLog(archive, errMessage)
}

// Create the request
req, err := http.NewRequest("POST", staticFilesEndpoint, &body)
if err != nil {
errMessage := fmt.Sprintf("[ERR] - [BlocksPostgresRepository] - [deleteDependentArchives]: Unable to create the request: %s", err.Error())
repository.saveDeletionErrorLog(archive, errMessage)
}

// Send the request
client := &http.Client{}
res, err := client.Do(req)

// Forward error message if any
microserviceError := sharedInfrastructure.ParseMicroserviceError(res, err)
if microserviceError != nil {
errMessage := fmt.Sprintf("[ERR] - [BlocksPostgresRepository] - [deleteDependentArchives]: Microservice returned an error: %s", microserviceError.Error())
repository.saveDeletionErrorLog(archive, errMessage)
}
}
}

func (repository *BlocksPostgresRepository) saveDeletionErrorLog(archive *sharedEntities.StaticFileArchive, errorMessage string) {
// Log the error to the console
log.Println(errorMessage)

// Save the error log to the database
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

query := `
INSERT INTO files_deletion_error_logs (file_id, file_type, error_message)
VALUES ($1, $2, $3)
`

_, err := repository.Connection.ExecContext(ctx, query, archive.ArchiveUUID, archive.ArchiveType, errorMessage)
if err != nil {
log.Println("[ERR] - [BlocksPostgresRepository] - [saveDeletionErrorLog]: Unable to save the error log", err)
}
}
6 changes: 6 additions & 0 deletions src/shared/domain/entities/StaticFileArchive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package entities

type StaticFileArchive struct {
ArchiveUUID string `json:"archive_uuid"`
ArchiveType string `json:"archive_type"`
}

0 comments on commit 3ddaa80

Please sign in to comment.