Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
Neo Kusanagi committed Jan 8, 2025
2 parents 80c9bc2 + e2bb2fb commit 6e60b5d
Show file tree
Hide file tree
Showing 19 changed files with 337 additions and 323 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
// package com.odde.doughnut.controllers;
//
// import com.odde.doughnut.controllers.dto.NoteRealm;
// import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
// import org.springframework.web.bind.annotation.*;
// import org.springframework.web.multipart.MultipartFile;
//
// @RestController
// @RequestMapping("/api")
// public class RestObsidianImportController {
//
// public RestObsidianImportController() {}
//
// @PostMapping("/obsidian/{parentNoteId}/import")
// public NoteRealm importObsidianNotes(MultipartFile file, @PathVariable Integer parentNoteId)
// throws UnexpectedNoAccessRightException {
// // Implementation will be added later
// return null;
// }
// }
package com.odde.doughnut.controllers;

import com.odde.doughnut.controllers.dto.NoteRealm;
import com.odde.doughnut.entities.Note;
import com.odde.doughnut.entities.Notebook;
import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
import com.odde.doughnut.models.NoteViewer;
import com.odde.doughnut.models.UserModel;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("/api")
public class RestObsidianImportController {
private final UserModel currentUser;

public RestObsidianImportController(UserModel currentUser) {
this.currentUser = currentUser;
}

@PostMapping("/obsidian/{parentNoteId}/import")
public NoteRealm importObsidian(
@RequestParam("file") MultipartFile file,
@PathVariable("parentNoteId") @Schema(type = "integer") Integer parentNoteId)
throws UnexpectedNoAccessRightException {
currentUser.assertLoggedIn();

Notebook notebook =
currentUser.getEntity().getOwnership().getNotebooks().stream()
.filter(n -> n.getId().equals(parentNoteId))
.findFirst()
.orElseThrow(() -> new UnexpectedNoAccessRightException());

// TODO: Process zip file content
// For now, just return the head note
Note note = notebook.getHeadNote();
return new NoteViewer(currentUser.getEntity(), note).toJsonObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ void shouldNotAllowUnauthorizedAccess() {
}

@Nested
class DownloadNotebookAsZip {
class DownloadForObsidian {
private Notebook notebook;
private Note note1;
private Note note2;
Expand All @@ -307,12 +307,12 @@ void whenNotAuthorized() {
modelFactoryService.toUserModel(anotherUser),
testabilitySettings);
assertThrows(
UnexpectedNoAccessRightException.class, () -> controller.downloadNotebookAsZip(notebook));
UnexpectedNoAccessRightException.class, () -> controller.downloadForObsidian(notebook));
}

@Test
void whenAuthorizedShouldReturnZipWithMarkdownFiles() throws Exception {
byte[] zipContent = controller.downloadNotebookAsZip(notebook);
byte[] zipContent = controller.downloadForObsidian(notebook);

try (ByteArrayInputStream bais = new ByteArrayInputStream(zipContent);
ZipInputStream zis = new ZipInputStream(bais)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,81 +1,111 @@
// package com.odde.doughnut.controllers;
//
// import static org.hamcrest.MatcherAssert.assertThat;
// import static org.hamcrest.Matchers.*;
// import static org.junit.jupiter.api.Assertions.assertThrows;
//
// import com.odde.doughnut.controllers.dto.NoteRealm;
// import com.odde.doughnut.controllers.dto.NoteRealm;
// import com.odde.doughnut.entities.Note;
// import com.odde.doughnut.entities.User;
// import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
// import com.odde.doughnut.models.UserModel;
// import com.odde.doughnut.testability.MakeMe;
// import org.junit.jupiter.api.BeforeEach;
// import org.junit.jupiter.api.Test;
// import org.springframework.mock.web.MockMultipartFile;
// import org.springframework.web.multipart.MultipartFile;
//
// import java.io.ByteArrayOutputStream;
// import java.io.IOException;
// import java.util.zip.ZipEntry;
// import java.util.zip.ZipOutputStream;
//
// class RestObsidianImportControllerTests {
// private RestObsidianImportController controller;
// private MakeMe makeMe;
// private UserModel userModel;
// private Note parentNote;
//
// @BeforeEach
// void setup() {
// userModel = makeMe.aUser().toModelPlease();
// controller = new RestObsidianImportController();
// parentNote = makeMe.aNote().creatorAndOwner(userModel).please();
// }
//
// @Test
// void shouldImportObsidianNotesUnderParentNote() throws UnexpectedNoAccessRightException,
// IOException {
// // Create a mock zip file with Obsidian notes
// MultipartFile zipFile = createMockZipFile("Note 2.md", "# Note 2\nSome content");
//
// // Import the zip file under the parent note
// NoteRealm importedNote = controller.importObsidianNotes(zipFile, parentNote.getId());
//
// // Verify the imported note
// assertThat(importedNote.getNote().getTopicConstructor(), equalTo("Note 2"));
// assertThat(importedNote.getNote().getParent().getId(), equalTo(parentNote.getId()));
// }
//
// @Test
// void shouldThrowExceptionWhenUserHasNoAccessToParentNote() {
// // Create a note owned by a different user
// Note otherUsersNote = makeMe.aNote().creatorAndOwner(makeMe.aUser().please()).please();
// MultipartFile zipFile = createMockZipFile("Note 2.md", "# Note 2\nSome content");
//
// // Attempt to import under a note the user doesn't have access to
// assertThrows(UnexpectedNoAccessRightException.class, () ->
// controller.importObsidianNotes(zipFile, otherUsersNote.getId())
// );
// }
//
// private MultipartFile createMockZipFile(String filename, String content) {
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
// try (ZipOutputStream zos = new ZipOutputStream(baos)) {
// ZipEntry entry = new ZipEntry(filename);
// zos.putNextEntry(entry);
// zos.write(content.getBytes());
// zos.closeEntry();
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
//
// return new MockMultipartFile(
// "file",
// "notes.zip",
// "application/zip",
// baos.toByteArray()
// );
// }
// }
package com.odde.doughnut.controllers;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

import com.odde.doughnut.controllers.dto.NoteRealm;
import com.odde.doughnut.entities.Note;
import com.odde.doughnut.entities.Notebook;
import com.odde.doughnut.exceptions.UnexpectedNoAccessRightException;
import com.odde.doughnut.factoryServices.ModelFactoryService;
import com.odde.doughnut.models.UserModel;
import com.odde.doughnut.testability.MakeMe;
import java.io.IOException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@SpringBootTest
@ActiveProfiles("test")
@Transactional
class RestObsidianImportControllerTests {
@Autowired ModelFactoryService modelFactoryService;
@Autowired MakeMe makeMe;
private UserModel userModel;
private RestObsidianImportController controller;

@BeforeEach
void setup() {
userModel = makeMe.aUser().toModelPlease();
controller = new RestObsidianImportController(userModel);
}

@Nested
class ImportObsidianTest {
private Note note1;
private Notebook notebook;
private MockMultipartFile zipFile;

@BeforeEach
void setup() {
// Create notebook with Note1
notebook = makeMe.aNotebook().creatorAndOwner(userModel).please();
note1 =
makeMe
.aNote("Note 1")
.under(notebook.getHeadNote())
.details("Content of Note 1")
.please();

// Create mock zip file
zipFile =
new MockMultipartFile(
"file", "obsidian.zip", "application/zip", "# Note2\nContent of Note 2".getBytes());
}

// @Test
void shouldReturnNote1WhenUserHasAccess() throws UnexpectedNoAccessRightException, IOException {
// Act
NoteRealm response = controller.importObsidian(zipFile, notebook.getId());

// Assert
assertThat(response.getId(), equalTo(note1.getId()));
assertThat(response.getNote().getTopicConstructor(), equalTo("Note1"));
assertThat(response.getNote().getDetails(), equalTo("Content of Note 1"));
}

@Test
void shouldNotBeAbleToAccessNotebookIDontHaveAccessTo() {
// Arrange
UserModel otherUserModel = makeMe.aUser().toModelPlease();
Notebook otherNotebook = makeMe.aNotebook().creatorAndOwner(otherUserModel).please();

// Act & Assert
assertThrows(
UnexpectedNoAccessRightException.class,
() -> controller.importObsidian(zipFile, otherNotebook.getId()));
}

@Test
void shouldThrowExceptionForNonExistentNotebook() {
// Act & Assert
assertThrows(
UnexpectedNoAccessRightException.class, () -> controller.importObsidian(zipFile, 99999));
}

@Test
void shouldRequireUserToBeLoggedIn() {
// Arrange
userModel = makeMe.aNullUserModelPlease();
controller = new RestObsidianImportController(userModel);

// Act & Assert
ResponseStatusException exception =
assertThrows(
ResponseStatusException.class,
() -> controller.importObsidian(zipFile, notebook.getId()));

// Verify the correct status and message
assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode());
assertEquals("User Not Found", exception.getReason());
}
}
}
31 changes: 0 additions & 31 deletions e2e_test/features/notebooks/notebook_download_2.feature

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Feature: Notebook download for Obsidian
Feature: Notebook export for Obsidian
Background:
Given I am logged in as an existing user
And I have a notebook titled "Medical Notes"
Expand All @@ -7,10 +7,10 @@ Feature: Notebook download for Obsidian
| Patient Care | Basic patient care notes |
| Medications | Common medications list |
| Procedures | Standard procedures guide |
@ignore
Scenario: Download notebook as a flat zip file for Obsidian

Scenario: Export notebook as a flat zip file for Obsidian
When I go to Notebook page
And I click on the download for Obsidian option on notebook "Medical Notes"
And I click on the export for Obsidian option on notebook "Medical Notes"
Then I should receive a zip file containing
| Filename | Format |
| Patient Care.md | md |
Expand All @@ -20,17 +20,15 @@ Feature: Notebook download for Obsidian
And each markdown file should maintain its original content

@ignore
Scenario: Download notebook with special characters in title
Given I have a notebook titled "Pediatrics (2024)"
When I select the "Pediatrics (2024)" notebook
And I click on the download for Obsidian option
Scenario: Export notebook with special characters in title
When I select the "Medical Notes" notebook
And I click on the export for Obsidian option
Then I should receive a zip file with sanitized filenames
And all markdown files should be at the root level of the zip

@ignore
Scenario: Download empty notebook
Given I have an empty notebook titled "Empty Notes"
When I select the "Empty Notes" notebook
And I click on the download for Obsidian option
Then I should receive an empty zip file
And I should see a notification that the notebook is empty
Scenario: Export empty notebook
When I select the "Medical Notes" notebook
And I click on the export for Obsidian option
Then I should receive a zip file containing three files
And each markdown file should maintain its original content
3 changes: 2 additions & 1 deletion e2e_test/features/notebooks/notebook_import.feature
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Feature: Notebook Import
| Title | Parent Title |
| note 2 | note 1 |

@ignore
Scenario: Import notes from Obsidian

When I Import Obsidian data "import-one-child.zip" to note "note 1"
Then I should see "note 1" with these children
| note-title|
| note 2 |
14 changes: 14 additions & 0 deletions e2e_test/start/pageObjects/notePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,5 +375,19 @@ export const assumeNotePage = (noteTopology?: string) => {
},
}
},
importObsidianData(filename: string) {
clickNotePageMoreOptionsButton('more options')
// Find the label containing "Import from Obsidian" text
cy.contains('label', 'Import from Obsidian').within(() => {
cy.get('input[type="file"]').selectFile(
`cypress/fixtures/${filename}`,
{ force: true }
)
})
cy.pageIsNotLoading()
// Wait for success message
cy.contains('Import successful!')
return this
},
}
}
7 changes: 7 additions & 0 deletions e2e_test/step_definitions/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,10 @@ Then('I should get immediate feedback by showing the wrong answer', () => {
.assumeWrongAnswerPage()
.highlightCurrentChoice('europe')
})

When(
'I Import Obsidian data {string} to note {string}',
(filename: string, noteTitle: string) => {
start.jumpToNotePage(noteTitle).importObsidianData(filename)
}
)
Loading

0 comments on commit 6e60b5d

Please sign in to comment.