feature: move docs

This commit is contained in:
2026-07-27 11:52:13 -04:00
parent ae0939177c
commit 03c06c0dd9
20 changed files with 607 additions and 35 deletions

View File

@@ -827,6 +827,90 @@ func TestAPIDocumentSaveReturnsConflictForStaleBaseHash(t *testing.T) {
}
}
func TestAPIDocumentMoveKeepsStableIDAndRedirectsOldPath(t *testing.T) {
server := newAPITestServer(t)
token := server.token(t, auth.ScopeDocsRead, auth.ScopeDocsWrite)
documentsRequest := httptest.NewRequest(http.MethodGet, "/api/documents", nil)
documentsRequest.Header.Set("Authorization", "Bearer "+token)
documents := httptest.NewRecorder()
server.handler.ServeHTTP(documents, documentsRequest)
var list struct {
Documents []struct {
ID string `json:"id"`
Path string `json:"path"`
Hash string `json:"hash"`
} `json:"documents"`
}
if err := json.Unmarshal(documents.Body.Bytes(), &list); err != nil || len(list.Documents) != 1 {
t.Fatalf("decode documents: err=%v payload=%#v", err, list)
}
move := jsonRequest(http.MethodPost, "/api/documents/hello.md/move", map[string]string{
"path": "guides/welcome.md",
"baseHash": list.Documents[0].Hash,
})
move.Header.Set("Authorization", "Bearer "+token)
moved := httptest.NewRecorder()
server.handler.ServeHTTP(moved, move)
if moved.Code != http.StatusOK {
t.Fatalf("move status = %d, want %d; body=%s", moved.Code, http.StatusOK, moved.Body.String())
}
var payload struct {
ID string `json:"id"`
Path string `json:"path"`
URL string `json:"url"`
}
if err := json.Unmarshal(moved.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode move response: %v", err)
}
if payload.ID != list.Documents[0].ID || payload.Path != "guides/welcome.md" || payload.URL != "/docs/guides/welcome" {
t.Fatalf("move response = %#v", payload)
}
oldPathRequest := httptest.NewRequest(http.MethodGet, "/docs/hello", nil)
oldPathRequest.Header.Set("Authorization", "Bearer "+token)
oldPath := httptest.NewRecorder()
server.handler.ServeHTTP(oldPath, oldPathRequest)
if oldPath.Code != http.StatusFound || oldPath.Header().Get("Location") != payload.URL {
t.Fatalf("old path response = %d location=%q, want redirect to %q", oldPath.Code, oldPath.Header().Get("Location"), payload.URL)
}
stableRequest := httptest.NewRequest(http.MethodGet, "/documents/"+url.PathEscape(payload.ID), nil)
stableRequest.Header.Set("Authorization", "Bearer "+token)
stable := httptest.NewRecorder()
server.handler.ServeHTTP(stable, stableRequest)
if stable.Code != http.StatusFound || stable.Header().Get("Location") != payload.URL {
t.Fatalf("stable ID response = %d location=%q, want redirect to %q", stable.Code, stable.Header().Get("Location"), payload.URL)
}
}
func TestAPIDocumentMoveRequiresDestinationFolderWrite(t *testing.T) {
server := newAPITestServer(t)
viewer := server.viewerUser(t)
admin := auth.Principal{UserID: server.userID, Role: auth.RoleAdmin}
if err := server.auth.EnsureResourcePermission(context.Background(), admin, auth.ResourceDocument, "hello.md", auth.ResourcePermissionUpdate{
UserID: viewer.ID, Permission: auth.PermissionWrite,
}); err != nil {
t.Fatalf("grant source document write: %v", err)
}
page, err := server.docs.LoadSourcePage(context.Background(), "hello")
if err != nil {
t.Fatalf("load source document: %v", err)
}
token := server.tokenForUser(t, viewer.ID, auth.ScopeDocsRead, auth.ScopeDocsWrite)
move := jsonRequest(http.MethodPost, "/api/documents/hello.md/move", map[string]string{
"path": "private/welcome.md",
"baseHash": page.Hash,
})
move.Header.Set("Authorization", "Bearer "+token)
response := httptest.NewRecorder()
server.handler.ServeHTTP(response, move)
if response.Code != http.StatusForbidden {
t.Fatalf("move status = %d, want %d; body=%s", response.Code, http.StatusForbidden, response.Body.String())
}
}
func TestAPIDocumentCreateGrantsCreatorAdminPermission(t *testing.T) {
server := newAPITestServer(t)
token := server.token(t, auth.ScopeDocsRead, auth.ScopeDocsWrite)