Skip to content

Tell the user when a project cannot be loaded - #100

Open
Pascal-SAPUI5 wants to merge 1 commit into
InteractionEngineer:mainfrom
Pascal-SAPUI5:fix/surface-load-errors
Open

Tell the user when a project cannot be loaded#100
Pascal-SAPUI5 wants to merge 1 commit into
InteractionEngineer:mainfrom
Pascal-SAPUI5:fix/surface-load-errors

Conversation

@Pascal-SAPUI5

Copy link
Copy Markdown

Fixes #37.

Changing a project's password on the server left PayForMe showing empty lists
and no explanation. The same silence covered a deleted project, an unreachable
server, and an address that is not a Cospend or iHateMoney instance at all.

Cause

The cause is structural rather than a missing check. Both load publishers
declared Never as their failure type:

func loadBillsPublisher(_ project: Project) -> AnyPublisher<[Bill], Never>

That is a promise not to fail, so every failure had to be turned into a value
before it could leave NetworkService. An HTTP status other than 200 was
dropped by a compactMap returning nil, and a URLError became [] via
replaceError. An empty list is exactly what a project with no bills looks
like, so nothing upstream could tell a rejected password from an empty project
— and nothing did. Publishers.Zip in ProjectManager then simply never
emitted, and the UI kept whatever it had.

Change

Both publishers now fail with a LoadError:

enum LoadError: Error, Equatable, Identifiable {
    case unauthorized      // 401, 403 — usually the password was changed
    case notFound          // 404
    case http(Int)
    case connection        // URLError, or a response that was not HTTP
    case invalidResponse   // answered, but not with something we can decode
}

ProjectManager keeps the last failure in @Published var loadingError,
cleared by any load that works, and ContentView shows it. The message for a
rejected password names the fix — add the project again with the new password —
because that is the step the reporter needed and could not guess.

On the existing tests

Two tests pinned the old behaviour and are rewritten rather than deleted. One
of them already carried the note:

loadBillsPublisher must NOT emit on 404 — but should when proper feedback is
implemented

A further sixteen sink { _ in … } calls had to become the two-closure form:
sink(receiveValue:) only exists for Failure == Never. That compiler error
list is a fair map of everything that could previously swallow a failure
unnoticed.

Changes

  • PayForMe/Services/NetworkService.swiftLoadError, validate(_:_:),
    both load publishers
  • PayForMe/Services/ProjectManager.swift — keep and clear the last failure
  • PayForMe/Views/ContentView.swift — show it, and map each case to a message
  • PayForMe/Strings/* — six messages in all six localisations
  • PayForMeTests/LoadErrorTests.swift — new
  • PayForMeTests/NetworkRequestTests.swift, TestHelpers.swift — adjusted

Verifying

Seven new tests in LoadErrorTests:

  • testRejectedCredentialsReachTheCaller — the case from the issue: 401 arrives
    as .unauthorized instead of vanishing
  • testForbiddenCountsAsRejectedCredentials, testMembersFailTheSameWay
  • testAnUnexpectedStatusIsPassedThroughVerbatim — 500 stays 500
  • testAnUnreadableBodyIsAnInvalidResponse — a 200 full of HTML
  • testStatusCodesMapToTheRightCase
  • testEveryErrorHasATranslatedMessage — every case has its own message and
    none falls back to the raw key, which would otherwise ship
    "load_error_unauthorized" to the screen

By hand: add a project, change its password in Cospend, pull to refresh. Before,
the lists stayed as they were with no hint; now the app says the password was
rejected and what to do about it.

On the name

The type is LoadError, not ServerError, because NetworkService already
nests a ServerError of its own. A second one under the same name resolves to
the nested type inside the class and to the new one outside it, which the
compiler reports as "cannot assign value of type 'NetworkService.ServerError'
to type 'ServerError'".

Note

This also covers part of #53, where "Could not find server" is shown when it is
the project that could not be found — .notFound now says so.

The scope is deliberately the load path. The write operations
(postBillPublisher and friends) still report success as a Bool and would
benefit from the same treatment, but that is a separate change.

Changing a project's password on the server left PayForMe showing empty lists
and no explanation. The same happened for a deleted project, an unreachable
server, or an address that is not a Cospend or iHateMoney instance at all.

The cause was structural. Both load publishers declared `Never` as their
failure type, so every failure had to become a value before it could leave
NetworkService: an HTTP status other than 200 was dropped by a `compactMap`,
and a URLError was turned into `[]` by `replaceError`. An empty list is exactly
what a project with no bills looks like, so nothing upstream could tell the two
apart — and nothing did.

Both publishers now fail with a `LoadError`, ProjectManager keeps the last
failure, and ContentView shows it. The message names what happened: a rejected
password suggests adding the project again with the new one, which is the
action the reporter needed and could not guess.

The type is called LoadError rather than ServerError because NetworkService
already nests a `ServerError` of its own, and a second one under the same name
resolves differently inside and outside the class.

The two existing tests that pinned the old behaviour are rewritten rather than
deleted — one of them already carried the note "should [emit] when proper
feedback is implemented".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Empty bills & project members after password change on cospend

1 participant