Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow owner to disable guest links #646

Merged
merged 32 commits into from
Jan 11, 2025
Merged

Conversation

x1uc
Copy link
Contributor

@x1uc x1uc commented Jan 4, 2025

feat: Allow owner to disable guest links

This PR implements the feature request in #543.

To summarize what has been done:

  • Added a column (disabled) to the guest_links table & the GuestLink struct.
  • Updated the isActive check for guest links to include the disabled field.
  • Added a column on the guest links front page, with a button to enable/disable the link.
  • Added new APIs related to managing guest links' disabled state.

I’m a beginner, and I believe there may be many areas where my work lacks standardization from your perspective. Please point them out, and I will work on improving. I’m very glad to hear your suggestions, which will not only improve the quality of this project but also enhance my programming skills.


Screen
output

Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! You did a good job of matching a lot of the conventions in the codebase so that your changes fit in elegantly.

I added some notes on places where I think we could improve, but I think this is off to a great start.

func (s Store) DisableGuestLink(id picoshare.GuestLinkID) error {
log.Printf("Disable guest link %s", id)

tx, err := s.ctx.BeginTx(context.Background(), nil)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transactions are useful when we have multiple SQL statements, and we want to rollback previous statements if any in the transaction fail.

When there's only one statement, we can just execute it immediately, like in InsertGuestLink.

(ditto below)

@@ -127,17 +129,62 @@ func (s Store) DeleteGuestLink(id picoshare.GuestLinkID) error {
return tx.Commit()
}

func (s Store) DisableGuestLink(id picoshare.GuestLinkID) error {
log.Printf("Disable guest link %s", id)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log messages are not capitalized. Also, for concistency, it should be "disabling" so this should look like:

log.Printf("disabling guest link %s", id)

(ditto for the enable function)

@@ -125,6 +173,7 @@ <h1 class="title">Guest Links</h1>
<th>Max Upload Size</th>
<th>Uploads</th>
<th class="has-text-right">Actions</th>
<th>Enable/Disable</th>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can disable/enable be one of the actions rather than having its own column? I think the ban icon would work:

https://fontawesome.com/icons/ban?s=solid

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need an icon to represent 'revoke ban'. Which icon would be a good opposite of the ban icon? I’ve looked at a few, but none of them seem very appropriate.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the checkbox you're using now works well enough.

@@ -90,6 +90,9 @@ func (s Server) guestLinkIndexGet() http.HandlerFunc {
"isActive": func(gl picoshare.GuestLink) bool {
return gl.IsActive()
},
"isDisabled": func(gl picoshare.GuestLink) bool {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to do this, as the template can just call the struct's .IsDisabled function directly.

isActive set the wrong precedent, so I deleted that in #647.

@@ -14,6 +14,8 @@ func (s *Server) routes() {
authenticatedApis.HandleFunc("/entry/{id}", s.entryDelete()).Methods(http.MethodDelete)
authenticatedApis.HandleFunc("/guest-links", s.guestLinksPost()).Methods(http.MethodPost)
authenticatedApis.HandleFunc("/guest-links/{id}", s.guestLinksDelete()).Methods(http.MethodDelete)
authenticatedApis.HandleFunc("/guest-links/enable/{id}", s.guestLinksEnable()).Methods(http.MethodPatch)
authenticatedApis.HandleFunc("/guest-links/disable/{id}", s.guestLinksDisable()).Methods(http.MethodPatch)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think /guest-links/{id}/enable is a bit more idiomatic and consistent with our other routes.

Also, these seem like they should be PUT rather than PATCH as they're idempotent:

In comparison with PUT, a PATCH serves as a set of instructions for modifying a resource, whereas PUT represents a complete replacement of the resource. A PUT request is always idempotent (repeating the same request multiple times results in the resource remaining in the same state), whereas a PATCH request may not always be idempotent. For instance, if a resource includes an auto-incrementing counter, a PUT request will overwrite the counter (since it replaces the entire resource), but a PATCH request may not.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH

if (toggleBtn.getAttribute("aria-label") === "Enable") {
guestLinkEnable(id)
.then(() => {
const button = toggleBtn;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we defining a new variable? Couldn't we just keep using toggleBtn?

button.classList.remove("is-danger");
button.classList.add("is-success");
button.setAttribute("aria-label", "Disable");
button.innerHTML =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I try to avoid assigning .innerHTML since it's typically more powerful than we need. In this case, it looks like the only change is adding or removing fa-check-square. Can we do that with classList instead?

@x1uc x1uc force-pushed the issue#543 branch 3 times, most recently from 288ffcb to e171ed5 Compare January 6, 2025 16:22
@x1uc
Copy link
Contributor Author

x1uc commented Jan 6, 2025

I apologize for the two unsuccessful pushes on CircleCI. Will Circle send you any email notifications about the CI failure and disturb you?
I noticed that the CONTRIBUTE.md mentions using npm run format to format the frontend code, but it doesn't seem to work. I think the script should be added to the package.json like this:

"scripts": {
  "format": "prettier --write ."
}

@@ -55,12 +54,12 @@ func (gl GuestLink) IsExpired() bool {
return time.Now().After(time.Time(gl.UrlExpires))
}

func (gl GuestLink) IsDisabled() bool {
return gl.Disabled != GuestLinkDisabled && *gl.Disabled == 1
func (gl GuestLink) IsDisabledLink() bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this function isn't actually needed, I only realized that after reviewing it later. I read your article on code reviews, and you mentioned that submitting code at night can sometimes cause trouble, and I think that's true.

@mtlynch
Copy link
Owner

mtlynch commented Jan 6, 2025

I apologize for the two unsuccessful pushes on CircleCI. Will Circle send you any email notifications about the CI failure and disturb you?

No, it's fine. I don't receive any of those emails, so feel free to build as much as you'd like.

I noticed that the CONTRIBUTE.md mentions using npm run format to format the frontend code, but it doesn't seem to work.

Ah, yes, thanks for letting me know. I've fixed the instructions in #648.

@x1uc
Copy link
Contributor Author

x1uc commented Jan 7, 2025

Changelist:

  • Rename the field from diabled to is_disabled .
  • Make disable/enable one of the actions.
  • Replace innerHtml manipulation with classList.toggle for cleaner DOM updates.
  • Refactor the API to follow RESTful conventions.
  • Implement end-to-end tests for this feature.
  • Add unit tests for guestLinksDisable and guestLinksEnable functions.

@mtlynch
Copy link
Owner

mtlynch commented Jan 9, 2025

Thanks for updating! I started a review, but Github seems to be having an outage that's preventing me from adding more notes to the review. I'll try again tomorrow.

Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on adding tests! You're continuing to match the conventions of the codebase really well and fit the feature to PicoShare's existing architecture.

I have a few smaller notes, but we're getting close to the finish line.

expect(guestLinkRouteValue).not.toBeNull();
const guestLinkRoute = String(guestLinkRouteValue);

// Disable the guest link
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) Can we add a period at the end?

(ditto for all inline comments)

s.Router().ServeHTTP(rec, req)
res := rec.Result()

if status := res.StatusCode; status != http.StatusNoContent {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the if got, want pattern here?

Sorry for the confusion. You did the right thing by following the existing conventions in the file, but I wrote the file before I learned the technique, so now I'm trying to write all new tests in the if got, want style.

This actually inspired me to write a blog post today about the pattern.

}

if !gl.IsDisabled {
t.Fatalf("expected guest link to be disabled, got: %v", gl)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be t.Errorf. In general, we reserve t.Fatalf for cases where it's impossible for the test to continue. So, the previous instances make sense as t.Fatalf because there's no point in continuing if the status code is wrong or it can't retrieve the guest link, but just having the wrong state shouldn't really be fatal.

It's kind of moot because it's the last assertion in the test, but still.

s.Router().ServeHTTP(rec, req)
res := rec.Result()

// File doesn't exist, but there's no error for enabling a non-existent file.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this fail? I feel like more sensible behavior would be for this to 404.

// Switch the button color & icon
const buttonClasses = button.classList;
buttonClasses.toggle("is-success");
buttonClasses.toggle("is-danger");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like the extra variable isn't worth it here. I think this way is actually clearer:

button.classList.toggle("is-success");
button.classList.toggle("is-danger");

(ditto for iconClasses)

@@ -82,6 +86,60 @@
});
});
});

const guestLinkButtonStates = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pull this into the event handler for toggleBtn.addEventListener("click"?

In general, I like variables to have as small a scope as possible to do what we need. Plus, it's helpful to have the declaration close to where we use it.

},
Disable: {
apiCall: guestLinkDisable,
message: "Successfully disabled the guest link.",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this "Guest link disabled" to match the structure of the "enable" message?

const guestLinkButtonStates = {
Enable: {
apiCall: guestLinkEnable,
message: "Guest link enabled successfully.",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can cut out "successfully" for brevity.

@x1uc
Copy link
Contributor Author

x1uc commented Jan 9, 2025

Change List

  • Refactored tests to use the if got, want style.
  • Refactored unit tests to adopt the table-driven test approach, reducing the number of test functions from 6 to 2.
  • Made several grammatical corrections.
  • Updated response status for invalid or non-existent guest_link id from 204 No Content to 404 Not Found .

@x1uc x1uc requested a review from mtlynch January 9, 2025 10:54
Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Just a little tidying up in the unit test, and then we should be ready to merge.

}

// Copy the values that we can't predict in advance.
tt.expected.Created = gl.Created
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an artifact of old tests we shouldn't repeat. I did this before I added the Clock interface, so I couldn't predict the Created time because it was just whatever time the test ran. But with the Clock interface, we control the creation time, so we don't have to mess with the time after.

I fixed this in the other test as #649.

description: "disable an invalid guest link",
payload: "i-am-an-invalid-link",
operation: disableOperation,
expected: http.StatusNotFound,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's an invalid guest link ID (i.e., contains illegal characters) that should be http.StatusBadRequest.

disableOperation := createOperation("disable")
enableOperation := createOperation("enable")

//Function to create a new guest link.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after //. Should be // Function.

tt.expected.Created = gl.Created
tt.expected.ID = picoshare.GuestLinkID(response.ID)

if !reflect.DeepEqual(gl, tt.expected) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we switch this to if got, want? Like

if got, want := gl, tt.expected; !reflect.DeepEqual(got, want) {

Sorry for the bad examples elsewhere. I just fixed them in #650


func TestGuestLinkToggleInValidRequest(t *testing.T) {

type operation struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is closer, but for both of these tests, we're adding a bit too much indirection and complexity in the test itself.

For test code, we want to minimize indirection so that we can verify it's correct by inspection. Google has a good blog post about this:

We have to change the style in test code. In production code, it's more important to avoid duplicating logic, but in tests, I'm willing to sacrifice duplication if it makes the tests more obvious.

In table-driven tests, the tables should be plain data, with as little runtime processing as possible.

Like, compare the complexity of understanding a direct route like this /api/guest-links/abcdefgh23456789 as opposed to seeing fmt.Sprintf("/api/guest-links/%s/%s", id, action) and then tracing back to figure out what the value of id and action would be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I read the article, and it makes sense. The issue in TestGuestLinkOperations can be easily avoided by modifying the code as follows:

[]struct {
    description string
    url         string
    expected    int
}{
    {
        description: "disable a nonexistent guest link",
        url:         "/api/guest-links/abcdefgh23456789/disable",
        expected:    http.StatusNotFound,
    },
}

However, the function TestGuestLinkToggleInvalidRequest also uses fmt.Sprintf("/api/guest-links/%s/%s", id, action) to construct URLs dynamically because the guest_link id is uncertain before the test runs. I'm not sure how to address this. Could you provide some advice on how to improve or refactor the function?

Additionally, I’m new to testing. In the codebase, I couldn’t find any indirect functions (like createLink or createOperation) being used in tests. Is it acceptable to use these indirect functions in tests, or is it better to avoid them for better test practices?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I read the article, and it makes sense. The issue in TestGuestLinkOperations can be easily avoided by modifying the code as follows:

Exactly! I think that version is much clearer.

However, the function TestGuestLinkToggleInvalidRequest also uses fmt.Sprintf("/api/guest-links/%s/%s", id, action) to construct URLs dynamically because the guest_link id is uncertain before the test runs. I'm not sure how to address this. Could you provide some advice on how to improve or refactor the function?

I think we should populate the datastore with a guest link we specify at the start of the test. That way, we know the ID. See TestGuestUpload as an example.

Additionally, I’m new to testing. In the codebase, I couldn’t find any indirect functions (like createLink or createOperation) being used in tests. Is it acceptable to use these indirect functions in tests, or is it better to avoid them for better test practices?

I think we should avoid them, as it forces the reader to jump around the file to understand the test logic. It's best if the reader can read the test from top to bottom without having to jump to previous function/variable definitions to understand what the test is doing.

@x1uc
Copy link
Contributor Author

x1uc commented Jan 10, 2025

Change List:

  • Removed the createLink and createOperation methods.
  • Eliminate uncertain test values.
  • Replaced concatenated strings with direct URLs.
  • Changed HTTP response status for invalid guest_link id from 404 Not Found to 400 Bad Request.

@x1uc x1uc requested a review from mtlynch January 10, 2025 06:58
Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few small things on the test to wrap up.

I know this has been a long review, so if you'd prefer me to do the last bit of tidying, I can do that.

@@ -346,3 +345,193 @@ func TestDeleteInvalidGuestLink(t *testing.T) {
t.Fatalf("DELETE returned wrong status code: got %v want %v", status, http.StatusBadRequest)
}
}

func TestGuestLinkToggleInValidRequest(t *testing.T) {
type testCase struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define this inline like we do elsewhere?

for _, tt := range []struct {
description string
payload string
currentTime time.Time
expected picoshare.GuestLink
status int
}{

description: "guest_link operation: disable",
operationUrls: []string{
"/api/guest-links/abcdefgh23456789/disable",
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do a single API request in each testcase? I don't think we have any other tests where we call the endpoint in a loop.

I like the simplicity of:

  1. Set up the server's state beforehand
  2. Send a single request that changes the server's state
  3. Test the server's state after that request

@@ -346,3 +345,193 @@ func TestDeleteInvalidGuestLink(t *testing.T) {
t.Fatalf("DELETE returned wrong status code: got %v want %v", status, http.StatusBadRequest)
}
}

func TestGuestLinkToggleInValidRequest(t *testing.T) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names seem to be flipped because these other test is called "TestGuestLinkOperations" but that's testing invalid input.

}
}

func TestGuestLinkOperations(t *testing.T) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to merge this into the test above? I just did this with the creation tests:

func TestGuestLinksPost(t *testing.T) {
for _, tt := range []struct {
description string
payload string
currentTime time.Time
expected picoshare.GuestLink
status int
}{
{
description: "minimally populated request",
payload: `{
"label": null,
"urlExpirationTime":"2030-01-02T03:04:25Z",
"fileLifetime":"876000h0m0s",
"maxFileBytes": null,
"maxFileUploads": null
}`,
currentTime: mustParseTime("2024-01-01T00:00:00Z"),
expected: picoshare.GuestLink{
Created: mustParseTime("2024-01-01T00:00:00Z"),
Label: picoshare.GuestLinkLabel(""),
UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"),
FileLifetime: picoshare.FileLifetimeInfinite,
MaxFileBytes: picoshare.GuestUploadUnlimitedFileSize,
MaxFileUploads: picoshare.GuestUploadUnlimitedFileUploads,
},
status: http.StatusOK,
},
{
description: "fully populated request",
payload: `{
"label": "For my good pal, Maurice",
"urlExpirationTime":"2030-01-02T03:04:25Z",
"fileLifetime":"876000h0m0s",
"maxFileBytes": 1048576,
"maxFileUploads": 1
}`,
currentTime: mustParseTime("2024-01-01T00:00:00Z"),
expected: picoshare.GuestLink{
Created: mustParseTime("2024-01-01T00:00:00Z"),
Label: picoshare.GuestLinkLabel("For my good pal, Maurice"),
UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"),
FileLifetime: picoshare.FileLifetimeInfinite,
MaxFileBytes: makeGuestUploadMaxFileBytes(1048576),
MaxFileUploads: makeGuestUploadCountLimit(1),
},
status: http.StatusOK,
},
{
description: "guest file expires in 1 day",
payload: `{
"label": "For my good pal, Maurice",
"urlExpirationTime":"2030-01-02T03:04:25Z",
"fileLifetime":"24h0m0s",
"maxFileBytes": 1048576,
"maxFileUploads": 1
}`,
currentTime: mustParseTime("2024-01-01T00:00:00Z"),
expected: picoshare.GuestLink{
Created: mustParseTime("2024-01-01T00:00:00Z"),
Label: picoshare.GuestLinkLabel("For my good pal, Maurice"),
UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"),
FileLifetime: picoshare.NewFileLifetimeInDays(1),
MaxFileBytes: makeGuestUploadMaxFileBytes(1048576),
MaxFileUploads: makeGuestUploadCountLimit(1),
},
status: http.StatusOK,
},
{
description: "guest file expires in 30 days",
payload: `{
"label": "For my good pal, Maurice",
"urlExpirationTime":"2030-01-02T03:04:25Z",
"fileLifetime":"720h0m0s",
"maxFileBytes": 1048576,
"maxFileUploads": 1
}`,
currentTime: mustParseTime("2024-01-01T00:00:00Z"),
expected: picoshare.GuestLink{
Created: mustParseTime("2024-01-01T00:00:00Z"),
Label: picoshare.GuestLinkLabel("For my good pal, Maurice"),
UrlExpires: mustParseExpirationTime("2030-01-02T03:04:25Z"),
FileLifetime: picoshare.NewFileLifetimeInDays(30),
MaxFileBytes: makeGuestUploadMaxFileBytes(1048576),
MaxFileUploads: makeGuestUploadCountLimit(1),
},
status: http.StatusOK,
},
{
description: "empty string",
payload: "",
status: http.StatusBadRequest,
},
{
description: "empty payload",
payload: "{}",
status: http.StatusBadRequest,
},
{
description: "invalid label field (non-string)",
payload: `{
"label": 5,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": null,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "invalid label field (too long)",
payload: fmt.Sprintf(`{
"label": "%s",
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": null,
"maxFileUploads": null
}`, strings.Repeat("A", 201)),
status: http.StatusBadRequest,
},
{
description: "missing urlExpirationTime field",
payload: `{
"label": null,
"maxFileBytes": null,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "invalid expirationTime field",
payload: `{
"label": null,
"urlExpirationTime": 25,
"maxFileBytes": null,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "negative maxFileBytes field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": -5,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "decimal maxFileBytes field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": 1.5,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "too low a maxFileBytes field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": 1,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "zero maxFileBytes field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": 0,
"maxFileUploads": null
}`,
status: http.StatusBadRequest,
},
{
description: "negative maxFileUploads field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": null,
"maxFileUploads": -5
}`,
status: http.StatusBadRequest,
},
{
description: "decimal maxFileUploads field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": null,
"maxFileUploads": 1.5
}`,
status: http.StatusBadRequest,
},
{
description: "zero maxFileUploads field",
payload: `{
"label": null,
"urlExpirationTime":"2025-01-01T00:00:00Z",
"maxFileBytes": null,
"maxFileUploads": 0
}`,
status: http.StatusBadRequest,
},
} {
t.Run(tt.description, func(t *testing.T) {
dataStore := test_sqlite.New()
c := mockClock{tt.currentTime}
s := handlers.New(mockAuthenticator{}, &dataStore, nilSpaceChecker, nilGarbageCollector, c)
req, err := http.NewRequest("POST", "/api/guest-links", strings.NewReader(tt.payload))
if err != nil {
t.Fatal(err)
}
req.Header.Add("Content-Type", "text/json")
rec := httptest.NewRecorder()
s.Router().ServeHTTP(rec, req)
res := rec.Result()
if got, want := res.StatusCode, tt.status; got != want {
t.Fatalf("status=%d, want=%d", got, want)
}
if tt.status != http.StatusOK {
return
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal("failed to read response body")
}
var response handlers.GuestLinkPostResponse
err = json.Unmarshal(body, &response)
if err != nil {
t.Fatalf("response is not valid JSON: %v", body)
}
gl, err := dataStore.GetGuestLink(picoshare.GuestLinkID(response.ID))
if err != nil {
t.Fatalf("failed to retrieve guest link from datastore: %v", err)
}
// Copy the ID, which we can't predict in advance.
tt.expected.ID = picoshare.GuestLinkID(response.ID)
if got, want := gl, tt.expected; !reflect.DeepEqual(got, want) {
t.Fatalf("guestLink=%+v, want=%+v", got, want)
}
})
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean merging this test into TestGuestLinksPost? Hmm, I’m not sure about that, as I believe TestGuestLinksPost is specifically for testing POST /api/guest-links. Could you offer some suggestions on how to approach this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, no I'm saying merge together TestGuestLinkToggleInValidRequest and TestGuestLinkOperations


for _, tt := range []testCase{
{
description: "guest_link operation: disable",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the descriptions be what we are saying the endpoint does. For example, for this one, I'd say, "disabling active guest link succeeds"

},
},
{
description: "guest_link operation: enable",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd describe this one as "enabling guest link succeeds but has no effect when guest link is not disabled"

@x1uc
Copy link
Contributor Author

x1uc commented Jan 10, 2025

ChangeList:
0. Define struct like otherwise.

  1. Add is_disabled field in test struct.
  2. Do a single API request in each test case.
  3. Refactor: unify two tests into a single one.
  4. Change test description.

@x1uc x1uc requested a review from mtlynch January 10, 2025 17:16
Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a a small bit of tidying left.

@@ -347,3 +346,164 @@ func TestDeleteInvalidGuestLink(t *testing.T) {
t.Fatalf("status=%d, want=%d", got, want)
}
}

func TestEnableDisableGuestLink(t *testing.T) {

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) Can we delete this blank line?

t.Run(tt.description, func(t *testing.T) {
dataStore := test_sqlite.New()

if tt.status == http.StatusNoContent {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change this to if !tt.guestLinkInStore.Empty() {?

I had trouble understand at first why we'd condition based on the HTTP response code when we hadn't even sent the HTTP request. I think checking based on whether the guest link is populated is the more direct way of testing whether we should add it into the mock store.

res := rec.Result()

if got, want := res.StatusCode, tt.status; got != want {
t.Errorf("%s: handler returned wrong status code: got %v want %v",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be t.Fatalf. If the status code is wrong, there's no sense in checking the response data.

And can we simplify to this to match the other tests?

if got, want := res.StatusCode, tt.status; got != want {
  t.Fatalf("status=%d, want=%d", got, want)
}

Including the description is an artifact from before I realized we could do t.Run.

}

if got, want := gl, tt.expected; !reflect.DeepEqual(got, want) {
t.Fatalf("guestLink=%+v, want=%+v", got, want)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be t.Errorf as it's not fatal for the result to not match expectations.

It's kind of moot since it's the last assertion in the test, but just good for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. This might involve another error. I wrote this code to mimic this one:

if got, want := gl, tt.expected; !reflect.DeepEqual(got, want) {
t.Fatalf("guestLink=%+v, want=%+v", got, want)
}

const oldIcon = button.querySelector("i");
const iconClasses = oldIcon.classList;
iconClasses.toggle("fa-check-square");
iconClasses.toggle("fa-ban");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this

button.querySelector("i").toggle("fa-check-square");
button.querySelector("i").toggle("fa-ban");

I think I gave this note in a previous round but it might have been dropped.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I’m very sorry. You previously pointed out another issue, and I didn’t notice similar errors elsewhere. My apologies.

@x1uc x1uc requested a review from mtlynch January 11, 2025 03:00
Copy link
Owner

@mtlynch mtlynch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much! This looks good. I'm just going to go over it with some small tweaks, but I should be able to merge this in in the next few days.

@mtlynch mtlynch merged commit 2e8c05f into mtlynch:master Jan 11, 2025
9 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Jan 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants