Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-lambda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Deploy to AWS Lambda (Optional)

on:
push:
tags: ['v*']
branches: [golang]
workflow_dispatch:
inputs:
environment:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Tests

on:
push:
branches: [main, dev]
branches: [golang]
pull_request:
branches: [main, dev]
branches: [golang]
workflow_dispatch:

env:
Expand Down
79 changes: 43 additions & 36 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ func parseNoteRequest(r *http.Request, bodyBytes []byte, clientIP string) (NoteR
return req, contentType, nil
}
req.Content = string(bodyBytes)
req.NoteID = extractPathNoteID(r)
log.Printf("[INFO] Received %d bytes from raw form body from %s", len(bodyBytes), clientIP)
return req, contentType, nil
}
Expand Down Expand Up @@ -626,6 +627,8 @@ func renderHTML(w http.ResponseWriter, noteID string, content string, r *http.Re
const printableEl = document.getElementById("printable");
const toastEl = document.getElementById("toast");

let saving = false;

function setStatus(text, state) {
statusText.textContent = text;
statusDot.className = 'status-dot ' + (state || 'ready');
Expand All @@ -649,43 +652,47 @@ func renderHTML(w http.ResponseWriter, noteID string, content string, r *http.Re

// Auto-save
function autoSave() {
if (textarea.value !== lastSaved) {
setStatus('Saving...', 'saving');

const saveUrl = currentNoteId ? appBase + 'noteid/' + currentNoteId : appBase;
fetch(saveUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ noteId: currentNoteId, content: textarea.value })
})
.then(function(response) {
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
return response.json();
})
.then(function(data) {
if (data.success) {
lastSaved = textarea.value;
currentNoteId = data.noteId;

var newPath = appBase + 'noteid/' + data.noteId;
if (window.location.pathname !== newPath && currentNoteId) {
window.history.replaceState({}, '', newPath);
document.getElementById('noteInfo').textContent = data.noteId;
}

setStatus('Saved', 'saved');
setTimeout(function() {
if (statusText.textContent === 'Saved') setStatus('Ready', 'ready');
}, 2000);
} else {
setStatus('Error: ' + (data.error || 'Save failed'), 'error');
if (saving || textarea.value === lastSaved) return;
saving = true;
setStatus('Saving...', 'saving');

const contentToSave = textarea.value;
const saveUrl = currentNoteId ? appBase + 'noteid/' + currentNoteId : appBase;
fetch(saveUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ noteId: currentNoteId, content: contentToSave })
})
.then(function(response) {
if (!response.ok) throw new Error('HTTP ' + response.status + ': ' + response.statusText);
return response.json();
})
.then(function(data) {
if (data.success) {
lastSaved = contentToSave;
currentNoteId = data.noteId;

var newPath = appBase + 'noteid/' + data.noteId;
if (window.location.pathname !== newPath && currentNoteId) {
window.history.replaceState({}, '', newPath);
document.getElementById('noteInfo').textContent = data.noteId;
}
})
.catch(function(err) {
console.error('Save error:', err);
setStatus('Error: ' + (err.message || 'Network error'), 'error');
});
}

setStatus('Saved', 'saved');
setTimeout(function() {
if (statusText.textContent === 'Saved') setStatus('Ready', 'ready');
}, 2000);
} else {
setStatus('Error: ' + (data.error || 'Save failed'), 'error');
}
})
.catch(function(err) {
console.error('Save error:', err);
setStatus('Error: ' + (err.message || 'Network error'), 'error');
})
.finally(function() {
saving = false;
});
}

setInterval(autoSave, 1000);
Expand Down
31 changes: 22 additions & 9 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ func TestHandleGetEmpty(t *testing.T) {
// TestHandleGetExisting tests GET request for existing note
func TestHandleGetExisting(t *testing.T) {
storage := NewMockStorage()
_ = storage.Write(context.Background(), "test123", "test content")
_ = storage.Write(context.Background(), "TEST23", "test content")

handler := HandleGet(storage)
req := httptest.NewRequest("GET", "/?note=test123", nil)
req := httptest.NewRequest("GET", "/?note=TEST23", nil)
rec := httptest.NewRecorder()

handler(rec, req)
Expand Down Expand Up @@ -131,7 +131,7 @@ func TestHandlePostExisting(t *testing.T) {
handler := HandlePost(storage)

payload := NoteRequest{
NoteID: "test123",
NoteID: "TEST23",
Content: "updated content",
}
body, _ := json.Marshal(payload)
Expand All @@ -155,7 +155,7 @@ func TestHandlePostExisting(t *testing.T) {
t.Errorf("Expected success=true")
}

if content, _ := storage.Read(context.Background(), "test123"); content != "updated content" {
if content, _ := storage.Read(context.Background(), "TEST23"); content != "updated content" {
t.Errorf("Expected content to be updated")
}
}
Expand Down Expand Up @@ -194,12 +194,12 @@ func TestHandlePostInvalidID(t *testing.T) {
// TestHandlePostDelete tests POST request with empty content (delete)
func TestHandlePostDelete(t *testing.T) {
storage := NewMockStorage()
_ = storage.Write(context.Background(), "test123", "original content")
_ = storage.Write(context.Background(), "TEST23", "original content")

handler := HandlePost(storage)

payload := NoteRequest{
NoteID: "test123",
NoteID: "TEST23",
Content: "",
}
body, _ := json.Marshal(payload)
Expand All @@ -215,7 +215,7 @@ func TestHandlePostDelete(t *testing.T) {
}

// Verify note was deleted
if content, _ := storage.Read(context.Background(), "test123"); content != "" {
if content, _ := storage.Read(context.Background(), "TEST23"); content != "" {
t.Errorf("Expected note to be deleted")
}
}
Expand Down Expand Up @@ -263,14 +263,14 @@ func TestParseNoteRequestJSONPath(t *testing.T) {

// TestParseNoteRequestFormAndRaw tests form parsing and raw body path fallback
func TestParseNoteRequestFormAndRaw(t *testing.T) {
form := "text=hi&noteId=FORMID"
form := "text=hi&noteId=FRMID"
req := httptest.NewRequest("POST", "/", bytes.NewBufferString(form))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
noteReq, _, err := parseNoteRequest(req, []byte(form), "127.0.0.1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if noteReq.NoteID != "FORMID" || noteReq.Content != "hi" {
if noteReq.NoteID != "FRMID" || noteReq.Content != "hi" {
t.Fatalf("unexpected form parse result: %#v", noteReq)
}

Expand All @@ -284,4 +284,17 @@ func TestParseNoteRequestFormAndRaw(t *testing.T) {
if noteReq.NoteID != "RAWID" || noteReq.Content != "raw body" {
t.Fatalf("unexpected raw parse result: %#v", noteReq)
}

// curl --data-binary sets Content-Type: application/x-www-form-urlencoded but body is plain text
// NoteID must be taken from the path, not ignored
req = httptest.NewRequest("POST", "/noteid/ABCDE", bytes.NewBufferString("hello content"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("User-Agent", "curl/8.5.0")
noteReq, _, err = parseNoteRequest(req, []byte("hello content"), "127.0.0.1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if noteReq.NoteID != "ABCDE" || noteReq.Content != "hello content" {
t.Fatalf("curl raw body via form content-type lost path noteID: %#v", noteReq)
}
}
12 changes: 6 additions & 6 deletions storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func TestLocalStorageRead(t *testing.T) {

// Write test file
testContent := "test content"
filePath := filepath.Join(tmpDir, "test123")
filePath := filepath.Join(tmpDir, "TEST23")
if err := os.WriteFile(filePath, []byte(testContent), 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
Expand All @@ -25,7 +25,7 @@ func TestLocalStorageRead(t *testing.T) {
}

// Test read
content, err := storage.Read(context.Background(), "test123")
content, err := storage.Read(context.Background(), "TEST23")
if err != nil {
t.Fatalf("Failed to read note: %v", err)
}
Expand Down Expand Up @@ -65,13 +65,13 @@ func TestLocalStorageWrite(t *testing.T) {
testContent := "test content"

// Test write
err = storage.Write(context.Background(), "test123", testContent)
err = storage.Write(context.Background(), "TEST23", testContent)
if err != nil {
t.Fatalf("Failed to write note: %v", err)
}

// Verify file exists
filePath := filepath.Join(tmpDir, "test123")
filePath := filepath.Join(tmpDir, "TEST23")
if _, err := os.Stat(filePath); err != nil {
t.Fatalf("Note file not created: %v", err)
}
Expand All @@ -92,13 +92,13 @@ func TestLocalStorageDelete(t *testing.T) {
}

// Create test file
filePath := filepath.Join(tmpDir, "test123")
filePath := filepath.Join(tmpDir, "TEST23")
if err := os.WriteFile(filePath, []byte("content"), 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}

// Test delete
err = storage.Delete(context.Background(), "test123")
err = storage.Delete(context.Background(), "TEST23")
if err != nil {
t.Fatalf("Failed to delete note: %v", err)
}
Expand Down
Loading
Loading